├── gtfs-validator-json ├── .gitignore ├── src │ ├── test │ │ ├── resources │ │ │ └── test_gtfs1.zip │ │ └── java │ │ │ └── com │ │ │ └── conveyal │ │ │ └── gtfs │ │ │ └── validator │ │ │ └── json │ │ │ └── test │ │ │ └── JsonOutputTest.java │ └── main │ │ └── java │ │ └── com │ │ └── conveyal │ │ └── gtfs │ │ └── validator │ │ └── json │ │ ├── LoadStatus.java │ │ ├── backends │ │ ├── FeedBackend.java │ │ └── FileSystemFeedBackend.java │ │ ├── serialization │ │ ├── Rectangle2DMixIn.java │ │ ├── Serializer.java │ │ ├── Rectangle2DDeserializer.java │ │ └── JsonSerializer.java │ │ ├── FeedValidationResult.java │ │ ├── JsonValidatorMain.java │ │ ├── FeedValidationResultSet.java │ │ └── FeedProcessor.java ├── validate_zipball.sh ├── README.md └── pom.xml ├── gtfs-validation-lib ├── .gitignore ├── src │ ├── test │ │ ├── resources │ │ │ ├── gtfs_bx10.zip │ │ │ ├── nyc_gtfs_si.zip │ │ │ ├── st_gtfs_bad.zip │ │ │ ├── test_gtfs1.zip │ │ │ ├── test_gtfs2.zip │ │ │ ├── st_gtfs_good.zip │ │ │ ├── brooklyn-a6-small.zip │ │ │ └── gtfs_two_agencies.zip │ │ └── java │ │ │ └── com │ │ │ └── conveyal │ │ │ └── gtfs │ │ │ ├── UnitTestBaseUtil.java │ │ │ ├── validator │ │ │ ├── ValidatorMainTest.java │ │ │ └── ValidatorMainIntegrationTest.java │ │ │ ├── ServiceIdHelperTest.java │ │ │ ├── StopOffShapeTest.java │ │ │ ├── GeoUtilsTests.java │ │ │ ├── GtfsStatisticsServiceCalendarDatesTest.java │ │ │ ├── GtfsStatisticsServiceTest.java │ │ │ ├── CalendarDateVerificationServiceLarge.java │ │ │ ├── CalendarDateVerificationServiceTest.java │ │ │ └── GtfsValidationServiceTest.java │ └── main │ │ └── java │ │ └── com │ │ └── conveyal │ │ └── gtfs │ │ ├── model │ │ ├── InputOutOfRange.java │ │ ├── comparators │ │ │ ├── StopTimeComparator.java │ │ │ ├── BlockIntervalComparator.java │ │ │ └── ShapePointComparator.java │ │ ├── Priority.java │ │ ├── HumanReadableServiceID.java │ │ ├── DuplicateStops.java │ │ ├── BlockInterval.java │ │ ├── ProjectedCoordinate.java │ │ ├── ValidationResult.java │ │ ├── InvalidValue.java │ │ ├── Statistic.java │ │ └── TripPatternCollection.java │ │ ├── service │ │ ├── StatisticsService.java │ │ ├── ServiceIdHelper.java │ │ ├── GeoUtils.java │ │ ├── CalendarDateVerificationService.java │ │ ├── impl │ │ │ └── GtfsStatisticsService.java │ │ └── GtfsValidationService.java │ │ └── validator │ │ └── ValidatorMain.java └── pom.xml ├── gtfs-validator-webapp ├── error.html ├── style.css ├── breadcrumb.html ├── list.html ├── package.json ├── validationrun.html ├── group.html ├── gulpfile.js ├── README.md ├── feedTable.html ├── feed.html ├── index.html └── validation.js ├── .travis.yml ├── .gitignore ├── install_precommit.sh ├── LICENSE ├── README.md └── pom.xml /gtfs-validator-json/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /gtfs-validation-lib/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /bin 3 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/error.html: -------------------------------------------------------------------------------- 1 |

<%= title %>

2 | <%= message %> 3 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/style.css: -------------------------------------------------------------------------------- 1 | .error { padding: 10px; border-radius: 5px } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: oraclejdk8 3 | 4 | cache: 5 | directories: 6 | - $HOME/.m2 7 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/resources/gtfs_bx10.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conveyal/gtfs-validator/HEAD/gtfs-validation-lib/src/test/resources/gtfs_bx10.zip -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/resources/nyc_gtfs_si.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conveyal/gtfs-validator/HEAD/gtfs-validation-lib/src/test/resources/nyc_gtfs_si.zip -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/resources/st_gtfs_bad.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conveyal/gtfs-validator/HEAD/gtfs-validation-lib/src/test/resources/st_gtfs_bad.zip -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/resources/test_gtfs1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conveyal/gtfs-validator/HEAD/gtfs-validation-lib/src/test/resources/test_gtfs1.zip -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/resources/test_gtfs2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conveyal/gtfs-validator/HEAD/gtfs-validation-lib/src/test/resources/test_gtfs2.zip -------------------------------------------------------------------------------- /gtfs-validator-json/src/test/resources/test_gtfs1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conveyal/gtfs-validator/HEAD/gtfs-validator-json/src/test/resources/test_gtfs1.zip -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/resources/st_gtfs_good.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conveyal/gtfs-validator/HEAD/gtfs-validation-lib/src/test/resources/st_gtfs_good.zip -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/resources/brooklyn-a6-small.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conveyal/gtfs-validator/HEAD/gtfs-validation-lib/src/test/resources/brooklyn-a6-small.zip -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/resources/gtfs_two_agencies.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conveyal/gtfs-validator/HEAD/gtfs-validation-lib/src/test/resources/gtfs_two_agencies.zip -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/InputOutOfRange.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model; 2 | 3 | public class InputOutOfRange extends Exception { 4 | 5 | /** 6 | * 7 | */ 8 | private static final long serialVersionUID = 1L; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.project 2 | **/.settings 3 | **/.classpath 4 | .project 5 | .settings/org.eclipse.m2e.core.prefs 6 | gtfs-validation-lib/src/main/java/com/conveyal/gtfs/service/impl/testing.java 7 | pre-commit.sh 8 | NL-20170119.gtfs.zip 9 | gtfs-validation-lib/src/test/resources/1NL-20170119.gtfs.zip 10 | -------------------------------------------------------------------------------- /gtfs-validator-json/src/main/java/com/conveyal/gtfs/validator/json/LoadStatus.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator.json; 2 | 3 | /** Why a GTFS feed failed to load */ 4 | public enum LoadStatus { 5 | SUCCESS, INVALID_ZIP_FILE, OTHER_FAILURE, MISSING_REQUIRED_FIELD, INCORRECT_FIELD_COUNT_IMPROPER_QUOTING; 6 | } 7 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/breadcrumb.html: -------------------------------------------------------------------------------- 1 | 2 | <% if (current.attr('id') == 'run') { %> 3 |
  • Home
  • 4 | <% } else { %> 5 |
  • Home
  • 6 | 7 |
  • <%= current.attr('data-name') %>
  • 8 | <% } %> 9 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/list.html: -------------------------------------------------------------------------------- 1 |
    2 |

    3 | <%= type %> (<%= errorCount %> warnings)   4 |

    5 |
    6 |
    7 | 8 | 9 | 10 |
    11 |
    12 | 13 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/comparators/StopTimeComparator.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model.comparators; 2 | 3 | import java.util.Comparator; 4 | 5 | import org.onebusaway.gtfs.model.StopTime; 6 | 7 | public class StopTimeComparator implements Comparator { 8 | 9 | public int compare(StopTime a, StopTime b) { 10 | return new Integer(a.getStopSequence()).compareTo(new Integer(b.getStopSequence())); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/comparators/BlockIntervalComparator.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model.comparators; 2 | 3 | import java.util.Comparator; 4 | 5 | import com.conveyal.gtfs.model.BlockInterval; 6 | 7 | public class BlockIntervalComparator implements Comparator { 8 | 9 | public int compare(BlockInterval a, BlockInterval b) { 10 | return new Integer(a.getStartTime()).compareTo(new Integer(b.getStartTime())); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/comparators/ShapePointComparator.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model.comparators; 2 | 3 | import java.util.Comparator; 4 | 5 | import org.onebusaway.gtfs.model.ShapePoint; 6 | 7 | // used in the TreeMap of ShapePoints 8 | public class ShapePointComparator implements Comparator { 9 | 10 | public int compare(ShapePoint a, ShapePoint b) { 11 | return new Integer(a.getSequence()).compareTo(new Integer(b.getSequence())); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/java/com/conveyal/gtfs/UnitTestBaseUtil.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs; 2 | 3 | import java.io.IOException; 4 | import java.io.OutputStream; 5 | import java.io.PrintStream; 6 | 7 | public class UnitTestBaseUtil { 8 | 9 | public UnitTestBaseUtil() { 10 | super(); 11 | } 12 | 13 | protected void setDummyPrintStream() { 14 | PrintStream dummyStream = new PrintStream(new OutputStream() { 15 | @Override 16 | public void write(int b) throws IOException {} 17 | }); 18 | System.setOut(dummyStream); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /gtfs-validator-json/validate_zipball.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Run a set of feed stored in a zipfile through the validator 3 | # Usage: validate_zipball.sh zipball.zip report.json 4 | # The JSON report generated can be displayed with the Web UI 5 | 6 | JARPATH=`dirname $0`/target/gtfs-validator-json.jar 7 | TEMPROOT=`mktemp -d` 8 | 9 | TEMP=${TEMPROOT}/$(basename -s .zip "$1") 10 | mkdir "$TEMPROOT" 11 | 12 | # unzip the gtfs 13 | unzip "$1" -d "$TEMP" 14 | 15 | # run the validator 16 | java -Xmx8G -jar "$JARPATH" "${TEMP}"/*.zip "$2" 17 | 18 | # clean up 19 | rm -rf "$TEMPROOT" 20 | 21 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "validation_output", 3 | 4 | "dependencies": { 5 | "backbone": "*", 6 | "underscore": "*", 7 | "bootstrap": "*", 8 | "jquery": "*" 9 | }, 10 | 11 | "devDependencies": { 12 | "gulp-rename": "^1.2.0", 13 | "gulp": "^3.8.10", 14 | "gulp-browserify": "^0.5.0", 15 | "browserify": "^6.3.4", 16 | "gulp-uglify": "^1.0.1", 17 | "gulp-autoprefixer": "^2.0.0", 18 | "vinyl-source-stream":"^1.1.0", 19 | "gulp-util":"^3.0.8", 20 | "gulp-clean-css":"^3.0.3", 21 | "node-underscorify":"0.0.14" 22 | }, 23 | 24 | "scripts": { 25 | "prebuild": "npm install", 26 | "build": "gulp" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/validationrun.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
    6 |
    7 |

    <%= name %>

    8 | Validation run on <%= date %>.
    9 | <%= feedCount %> feeds processed, <%= loadCount %> successfully. 10 | 11 |

    Feeds processed

    12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
    FeedLoaded successfully?WarningsRoutesTripsStop timesValid fromValid to
    21 |
    22 |
    23 | -------------------------------------------------------------------------------- /gtfs-validator-json/src/main/java/com/conveyal/gtfs/validator/json/backends/FeedBackend.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator.json.backends; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * A backend that stores feed data. 7 | * We could conceptually have one for dat, one for S3, one for robotic tape libraries . . . 8 | * Inspired by https://github.com/conveyal/transit-data-dashboard/blob/master/app/updaters/FeedStorer.java 9 | * 10 | * @author mattwigway 11 | */ 12 | public interface FeedBackend { 13 | /** 14 | * Get a feed from this backend. 15 | * @param feedId the feed to retrieve 16 | * @return a File object referring to the feed 17 | */ 18 | public File getFeed(String feedId); 19 | } 20 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/group.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | <%= at(0).attributes.problemType %> (<%= length %> warnings) 6 | <%= {HIGH: 'High priority', MEDIUM: 'Medium priority', LOW: 'Low priority', UNKNOWN: ''}[at(0).attributes.priority] %> 7 |    8 | 9 | 10 | Problem TypeAffected EntityAffected FieldDescription 11 | -------------------------------------------------------------------------------- /gtfs-validator-json/src/main/java/com/conveyal/gtfs/validator/json/serialization/Rectangle2DMixIn.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator.json.serialization; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFilter; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | /** 7 | * From https://github.com/conveyal/gtfs-data-manager/blob/master/app/controllers/api/Rectangle2DMixIn.java 8 | */ 9 | // ignore all by default 10 | @JsonFilter("bbox") 11 | public abstract class Rectangle2DMixIn { 12 | // stored as lon, lat 13 | @JsonProperty("west") public abstract double getMinX(); 14 | @JsonProperty("east") public abstract double getMaxX(); 15 | @JsonProperty("north") public abstract double getMaxY(); 16 | @JsonProperty("south") public abstract double getMinY(); 17 | } -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/Priority.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model; 2 | 3 | public enum Priority { 4 | /** 5 | * Something that is likely to break routing results, 6 | * e.g. stop times out of sequence or high-speed travel 7 | */ 8 | HIGH, 9 | 10 | /** 11 | * Something that is likely to break display, but still give accurate routing results, 12 | * e.g. broken shapes or route long name containing route short name. 13 | */ 14 | MEDIUM, 15 | 16 | /** 17 | * Something that will not affect user experience but should be corrected as time permits, 18 | * e.g. unused stops. 19 | */ 20 | LOW, 21 | 22 | /** 23 | * An error for which we do not have a priority 24 | */ 25 | UNKNOWN 26 | } 27 | -------------------------------------------------------------------------------- /gtfs-validator-json/src/main/java/com/conveyal/gtfs/validator/json/backends/FileSystemFeedBackend.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator.json.backends; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * A simple feed backend that simply gets data from the file system. Feed IDs are file paths. 7 | * Note that this means that there may be multiple IDs which refer to the same feed. 8 | * @author matthewc 9 | * 10 | */ 11 | public class FileSystemFeedBackend implements FeedBackend { 12 | 13 | /** 14 | * Get the feed at path feedId on the file system. This is trivial but allows for compatibility 15 | * with more complicated backends. 16 | * @param feedId The path the feed 17 | * @return a file object referring to the feed 18 | */ 19 | public File getFeed(String feedId) { 20 | return new File(feedId); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /gtfs-validator-json/README.md: -------------------------------------------------------------------------------- 1 | ## gtfs-validator-json: run feeds through the conveyal [gtfs-validator library](https://www.github.com/conveyal/gtfs-validator) and generate JSON-formatted reports 2 | 3 | This is a modular project; it can be used directly from the command line, e.g.: 4 | 5 | `java -Xmx6G -jar gtfs-validator-json.jar /path/to/gtfs.zip /path/to/gtfs2.zip . . . /path/to/output.json` 6 | 7 | or you can wire the classes together yourself. There are several important components: 8 | - FeedBackends: these represent a way to store feeds (for instance, file systems or s3 buckets). The only requirement is that each feed can be retrieved from an ID that can be stored as a string. 9 | - Serializers: these represent how to serialize a FeedValidationResultSet object to a stream. Right now we use JSON, one could also imagine many other potential formats. 10 | - FeedProcessor: this takes a feed, runs validation, and returns a FeedValidationResult. There is generally no reason to subclass this. -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/HumanReadableServiceID.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model; 2 | 3 | public class HumanReadableServiceID { 4 | 5 | private String depot; 6 | private String serviceId; 7 | 8 | public HumanReadableServiceID() { 9 | super(); 10 | depot = null; 11 | serviceId = "test"; 12 | } 13 | 14 | public String getDepot() { 15 | return depot; 16 | } 17 | 18 | public void setDepot(String depot) { 19 | this.depot = depot; 20 | } 21 | 22 | public String getServiceId() { 23 | return serviceId; 24 | } 25 | 26 | public void setServiceId(String serviceId) { 27 | this.serviceId = serviceId; 28 | } 29 | 30 | public void appendToServiceId(String serviceId) { 31 | this.serviceId += serviceId; 32 | } 33 | 34 | public String toString(){ 35 | StringBuilder sb = new StringBuilder(); 36 | if (depot != null) sb.append("for Depot " + this.depot + ", "); 37 | sb.append(this.serviceId); 38 | return sb.toString(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /install_precommit.sh: -------------------------------------------------------------------------------- 1 | # from http://eing.github.io/technology/2016/01/28/Git-Pre-Commit-Hooks-Part1/ 2 | { echo " 3 | git stash -q --keep-index 4 | # Using "mvn test" to run all unit tests and run plugins to assert 5 | # * code coverage threshold >= 85% (using surefire, enforcer plugins) 6 | # * FindBugs at low threshold errors (using findbugs-maven-plugin) 7 | # * Checkstyle has 0 errors (using maven-checkstyle-plugin) 8 | /usr/local/bin/mvn clean test 9 | RESULTS=\$? 10 | # Perform checks 11 | git stash pop -q 12 | if [ \$RESULTS -ne 0 ]; then 13 | echo Error: Commit criteria not met with one or more of the following issues, 14 | echo 1. Failure\(s\) in unit tests 15 | echo 2. Failure to meet 85% code coverage 16 | echo 3. Failure to meet low FindBugs threshold 17 | echo 4. Failure to meet 0 Checkstyle errors 18 | exit 1 19 | fi 20 | # You shall commit 21 | exit 0" 22 | } > pre-commit.sh 23 | pushd .git/hooks 24 | ln -sf ../../pre-commit.sh pre-commit 25 | chmod u+x pre-commit 26 | popd 27 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/DuplicateStops.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.onebusaway.gtfs.model.Stop; 6 | 7 | public class DuplicateStops implements Serializable { 8 | 9 | /** 10 | * 11 | */ 12 | private static final long serialVersionUID = 1L; 13 | public Stop stop1; 14 | public Stop stop2; 15 | 16 | public double distance; 17 | 18 | public DuplicateStops(Stop s1, Stop s2, double dist) { 19 | stop1 = s1; 20 | stop2 = s2; 21 | distance = dist; 22 | } 23 | 24 | public String getStop1Id() { 25 | return stop1.getId().getId(); 26 | } 27 | 28 | public String getStop2Id() { 29 | return stop2.getId().getId(); 30 | } 31 | 32 | public String getStopIds() { 33 | return this.getStop1Id() + "," + this.getStop2Id(); 34 | } 35 | 36 | public String toString() { 37 | return "Stops " + this.getStop1Id() + " and " + this.getStop2Id() + " are within " + this.distance + " meters"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/gulpfile.js: -------------------------------------------------------------------------------- 1 | // Gulp Dependencies 2 | var gulp = require('gulp'); 3 | var gp_rename = require('gulp-rename'); 4 | var browserify = require('browserify'); 5 | var source = require('vinyl-source-stream'); 6 | var cleanCSS = require("gulp-clean-css"); 7 | var underscorify = require("node-underscorify"); 8 | var gutil = require("gulp-util"); 9 | 10 | gulp.task('build-js', function() { 11 | return browserify('./validation.js') 12 | .transform(underscorify.transform()) 13 | .bundle() 14 | .on('error', function(err) { 15 | gutil.log(err); 16 | }) 17 | .pipe(source('build.js')) 18 | .pipe(gulp.dest('build')) 19 | }); 20 | 21 | gulp.task('build-css', function() { 22 | return gulp.src('./style.css') 23 | .pipe(gp_rename('build.css')) 24 | .pipe(cleanCSS()) 25 | .pipe(gulp.dest('build')); 26 | }); 27 | 28 | gulp.task('default', ['build-js', 'build-css'], function(){}); 29 | -------------------------------------------------------------------------------- /gtfs-validator-json/src/main/java/com/conveyal/gtfs/validator/json/serialization/Serializer.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator.json.serialization; 2 | 3 | import java.io.File; 4 | 5 | import com.conveyal.gtfs.validator.json.FeedValidationResultSet; 6 | 7 | /** 8 | * Basic code for a serializer for a feed validation result set 9 | * @author mattwigway 10 | */ 11 | public abstract class Serializer { 12 | protected FeedValidationResultSet results; 13 | 14 | /** 15 | * Create a serializer for these validation results 16 | * @param results 17 | */ 18 | public Serializer (FeedValidationResultSet results) { 19 | this.results = results; 20 | } 21 | 22 | /** Serialize the results wrapped by this class to whatever format is appropriate 23 | * @return serialized data 24 | * @throws Exception */ 25 | public abstract Object serialize () throws Exception; 26 | 27 | /** Serialize the results wrapped by this class, and write them to file 28 | * @param file the file to write results to 29 | * @throws Exception 30 | */ 31 | public abstract void serializeToFile (File file) throws Exception; 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Conveyal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/README.md: -------------------------------------------------------------------------------- 1 | ## gtfs-validator-webapp: inspect the output of the GTFS validator 2 | 3 | This is a UI for viewing the results of a GTFS validator run on a set of feeds. It reads in a JSON file, which for now must be called out.json in the same folder as index.html, and then displays the results in a browser-based UI. 4 | 5 | ### Building gtfs-validator-webapp 6 | * Install [node.js](https://nodejs.org/en/) in gtfs-validator-webapp directory. 7 | * Open command prompt and execute, `npm install` in gtfs-validator-webapp directory. This will install all the required dependencies using *package.json* file. 8 | * Finally execute command `gulp build` in gtfs-alidator-webapp directory. This will build the app using *gulpfile.js* and produces build.js and build.css files in gtfs-validator-webapp/build directory. 9 | 10 | ### Viewing web results 11 | Run gtfs-validator-json on a feed or set of feeds. Put the JSON file generated into the gtfs-validator-webapp directory, and call it `out.json`. 12 | 13 | After starting a local server, view the web report by providing URL [localhost:8080/index.html?report=http://localhost:8080/out.json](localhost:8080/index.html?report=http://localhost:8080/out.json). 14 | 15 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/feedTable.html: -------------------------------------------------------------------------------- 1 | <% if (loadStatus == 'SUCCESS') { %> 2 | <% var totalErrors = routes.length + stops.length + trips.length + shapes.length; %> 3 | 4 | 5 | <%= agencies.join(", ") %> 6 | 7 | Yes 8 | <%= totalErrors %> 9 | <%= routeCount %> 10 | <%= tripCount %> 11 | <%= stopTimesCount %> 12 | <% if (stopTimesCount > 0) { %> 13 | <%= startDate.toDateString() %> 14 | <%= endDate.toDateString() %> 15 | <% } else { %> 16 | -- 17 | <% } %> 18 | 19 | <% } else { %> 20 | 21 | 22 | <%= feedFileName %> 23 | 24 | No 25 | <%= loadFailureReason %> 26 | 27 | <% } %> 28 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/BlockInterval.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model; 2 | 3 | import org.onebusaway.gtfs.model.StopTime; 4 | import org.onebusaway.gtfs.model.Trip; 5 | 6 | 7 | public class BlockInterval implements Comparable { 8 | Trip trip; 9 | Integer startTime; 10 | StopTime firstStop; 11 | StopTime lastStop; 12 | 13 | public Trip getTrip() { 14 | return trip; 15 | } 16 | 17 | public void setTrip(Trip trip) { 18 | this.trip = trip; 19 | } 20 | 21 | public Integer getStartTime() { 22 | return startTime; 23 | } 24 | 25 | public void setStartTime(Integer startTime) { 26 | this.startTime = startTime; 27 | } 28 | 29 | public StopTime getFirstStop() { 30 | return firstStop; 31 | } 32 | 33 | public void setFirstStop(StopTime firstStop) { 34 | this.firstStop = firstStop; 35 | } 36 | 37 | public StopTime getLastStop() { 38 | return lastStop; 39 | } 40 | 41 | public void setLastStop(StopTime lastStop) { 42 | this.lastStop = lastStop; 43 | } 44 | 45 | public int compareTo(BlockInterval o) { 46 | return new Integer(this.firstStop.getArrivalTime()) 47 | .compareTo(new Integer(o.firstStop.getArrivalTime())); 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/feed.html: -------------------------------------------------------------------------------- 1 |

    Feed validation report for <%= loadStatus == 'SUCCESS' ? agencies.join(', ') : feedFileName %>

    2 | 3 | 4 | 5 | 6 | <% if (loadStatus == 'SUCCESS') { %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | <% if (stopTimesCount > 0) { %> 14 | 15 | <% } else { %> 16 | 17 | <% } %> 18 | <% } else { %> 19 | 20 | <% } %> 21 | 22 |
    Load Status<%= loadStatus %>
    Number of Agencies<%= agencyCount %>
    Number of Routes<%= routeCount %>
    Number of Trips<%= tripCount %>
    Number of Stop Times<%= stopTimesCount %>
    Service Range<%= startDate.toDateString() %> to <%= endDate.toDateString() %>
    -
    Reason<%= loadFailureReason %>
    23 | 24 |
    25 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/ProjectedCoordinate.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model; 2 | 3 | import org.opengis.referencing.operation.MathTransform; 4 | 5 | import com.conveyal.gtfs.service.GeoUtils; 6 | import com.vividsolutions.jts.geom.Coordinate; 7 | 8 | public class ProjectedCoordinate extends Coordinate { 9 | 10 | private static final long serialVersionUID = 2905131060296578237L; 11 | 12 | final private MathTransform transform; 13 | final private Coordinate refLatLon; 14 | 15 | public ProjectedCoordinate(MathTransform mathTransform, 16 | Coordinate to, Coordinate refLatLon) { 17 | this.transform = mathTransform; 18 | this.x = to.x; 19 | this.y = to.y; 20 | this.refLatLon = refLatLon; 21 | } 22 | 23 | public String epsgCode() { 24 | final String epsgCode = 25 | "EPSG:" + GeoUtils.getEPSGCodefromUTS(refLatLon); 26 | return epsgCode; 27 | } 28 | 29 | public Coordinate getReferenceLatLon() { 30 | return refLatLon; 31 | } 32 | 33 | public MathTransform getTransform() { 34 | return transform; 35 | } 36 | 37 | public double getX() { 38 | return this.x; 39 | } 40 | 41 | public double getY() { 42 | return this.y; 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Feed validation report 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 |
    25 |
    26 |
    27 |

    Loading . . .

    28 |
    29 |
    30 | 31 | 32 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/ValidationResult.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.Set; 5 | import java.util.TreeSet; 6 | import java.util.logging.Logger; 7 | 8 | 9 | public class ValidationResult implements Serializable { 10 | 11 | /** 12 | * 13 | */ 14 | private static final long serialVersionUID = 1L; 15 | 16 | private static Logger _log = Logger.getLogger(ValidationResult.class.getName()); 17 | 18 | public Set invalidValues = new TreeSet(); 19 | 20 | public void add(InvalidValue iv) { 21 | // _log.info(iv.toString()); 22 | invalidValues.add(iv); 23 | } 24 | 25 | public void append(ValidationResult vr) { 26 | invalidValues.addAll(vr.invalidValues); 27 | } 28 | 29 | public String toString(){ 30 | StringBuilder sb = new StringBuilder(); 31 | for (InvalidValue iv: invalidValues){ 32 | sb.append(iv); 33 | } 34 | return sb.toString(); 35 | } 36 | 37 | 38 | public boolean containsBoth(String one, String two, String type){ 39 | for (InvalidValue iv: invalidValues){ 40 | if (iv.problemDescription.contains(one) 41 | && iv.problemDescription.contains(two) 42 | && iv.affectedEntity == type) { 43 | return true; 44 | } 45 | } 46 | return false; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/java/com/conveyal/gtfs/validator/ValidatorMainTest.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator; 2 | 3 | import static org.junit.Assert.assertSame; 4 | 5 | import java.util.Date; 6 | import java.util.Optional; 7 | 8 | import org.junit.Test; 9 | 10 | public class ValidatorMainTest { 11 | 12 | @Test 13 | public void testEarlier() { 14 | 15 | Optional o = Optional.of(new Date(0L)); 16 | Date d = new Date(100000L); 17 | 18 | Date returned = ValidatorMain.getEarliestDate(o, d); 19 | 20 | assertSame(o.get(), returned); 21 | } 22 | 23 | @Test 24 | public void testEarlier2() { 25 | 26 | Optional o = Optional.of(new Date(10L)); 27 | Date d = new Date(0L); 28 | 29 | Date returned = ValidatorMain.getEarliestDate(o, d); 30 | 31 | assertSame(d, returned); 32 | } 33 | 34 | @Test 35 | public void testLater() { 36 | 37 | Optional o = Optional.of(new Date(100000L)); 38 | Date d = new Date(0L); 39 | 40 | Date returned = ValidatorMain.getLatestDate(o, d); 41 | 42 | assertSame(o.get(), returned); 43 | } 44 | @Test 45 | public void testLater2() { 46 | 47 | Optional o = Optional.of(new Date(0)); 48 | Date d = new Date(10L); 49 | 50 | Date returned = ValidatorMain.getLatestDate(o, d); 51 | 52 | assertSame(d, returned); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/java/com/conveyal/gtfs/ServiceIdHelperTest.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs; 2 | 3 | import static org.junit.Assert.assertNotNull; 4 | import static org.junit.Assert.assertTrue; 5 | 6 | import org.junit.Test; 7 | 8 | import com.conveyal.gtfs.model.HumanReadableServiceID; 9 | import com.conveyal.gtfs.service.ServiceIdHelper; 10 | 11 | public class ServiceIdHelperTest { 12 | 13 | @Test 14 | public void test() { 15 | ServiceIdHelper h = new ServiceIdHelper(); 16 | HumanReadableServiceID s = h.getHumanReadableCalendarFromServiceId("MTA NYCT_JG_C6-Weekday-SDon-BM"); 17 | 18 | assertNotNull(s); 19 | assertTrue(s.getDepot().equals("JG")); 20 | assertTrue(s.getServiceId().equals("WEEKDAY_SCHOOL_OPEN Next Day's Trips Starting Before Midnight")); 21 | 22 | s = h.getHumanReadableCalendarFromServiceId("MTA NYCT_FP_J6-Weekday"); 23 | assertTrue(s.getDepot().equals("FP")); 24 | assertTrue(s.getServiceId().equals("GOOD_FRIDAY")); 25 | 26 | s = h.getHumanReadableCalendarFromServiceId("MTABC_BPPC6-BP_C6-Weekday-30"); 27 | assertTrue(s.getDepot().equals("BP")); 28 | assertTrue(s.getServiceId().equals("WEEKDAY_SCHOOL_CLOSED")); 29 | 30 | 31 | s = h.getHumanReadableCalendarFromServiceId("AC Transit_1606SU-D4-Weekday-10"); 32 | assertTrue(s.getDepot().equals("D4")); 33 | 34 | s = h.getHumanReadableCalendarFromServiceId("42348"); 35 | assertTrue(s.getServiceId().equals("42348")); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/java/com/conveyal/gtfs/StopOffShapeTest.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs; 2 | 3 | import static org.junit.Assert.assertTrue; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | 8 | import org.junit.Before; 9 | import org.junit.BeforeClass; 10 | import org.junit.Test; 11 | import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; 12 | import org.onebusaway.gtfs.serialization.GtfsReader; 13 | 14 | import com.conveyal.gtfs.model.ValidationResult; 15 | import com.conveyal.gtfs.service.GtfsValidationService; 16 | 17 | 18 | public class StopOffShapeTest extends UnitTestBaseUtil{ 19 | 20 | static GtfsRelationalDaoImpl gtfsMDao = null; 21 | 22 | @BeforeClass 23 | public static void setUpClass() { 24 | GtfsReader reader = new GtfsReader(); 25 | gtfsMDao = new GtfsRelationalDaoImpl(); 26 | 27 | File gtfsFile = new File("src/test/resources/gtfs_bx10.zip"); 28 | 29 | try { 30 | reader.setInputLocation(gtfsFile); 31 | } catch (IOException e) { 32 | e.printStackTrace(); 33 | } 34 | 35 | reader.setEntityStore(gtfsMDao); 36 | 37 | try { 38 | reader.run(); 39 | } catch (IOException e) { 40 | e.printStackTrace(); 41 | } 42 | 43 | } 44 | @Before 45 | public void SetUp(){ 46 | setDummyPrintStream(); 47 | } 48 | 49 | @Test 50 | public void test() { 51 | GtfsValidationService valService = new GtfsValidationService(gtfsMDao); 52 | ValidationResult r = valService.listStopsAwayFromShape(130.0); 53 | assertTrue(r.invalidValues.size() > 0); 54 | assertTrue(r.toString().contains("103671")); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/service/StatisticsService.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.service; 2 | 3 | import java.awt.geom.Rectangle2D; 4 | import java.util.Date; 5 | import java.util.Optional; 6 | 7 | import com.conveyal.gtfs.model.Statistic; 8 | 9 | /** 10 | * Provides statistics for: 11 | * 12 | *
  • Agencies 13 | *
  • Routes 14 | *
  • Trips 15 | *
  • Stops 16 | *
  • Stop Times 17 | *
  • Calendar Date ranges 18 | *
  • Calendar Service exceptions 19 | * 20 | * @author dev 21 | * 22 | */ 23 | public interface StatisticsService { 24 | 25 | Integer getAgencyCount(); 26 | 27 | Integer getRouteCount(); 28 | 29 | Integer getTripCount(); 30 | 31 | Integer getStopCount(); 32 | 33 | Integer getStopTimesCount(); 34 | /* 35 | * As Calendar Dates are optional per the GTFS spec, this returns an Optional. 36 | * To retrieve a Date Object from this use the method, getCalendarDateStart().get() 37 | */ 38 | Optional getCalendarDateStart(); 39 | 40 | Optional getCalendarDateEnd(); 41 | 42 | Date getCalendarServiceRangeStart(); 43 | 44 | Date getCalendarServiceRangeEnd(); 45 | 46 | Integer getNumberOfDays(); 47 | 48 | Integer getRouteCount(String agencyId); 49 | 50 | Integer getTripCount(String agencyId); 51 | 52 | Integer getStopCount(String agencyId); 53 | 54 | Integer getStopTimesCount(String agencyId); 55 | 56 | Date getCalendarDateStart(String agencyId); 57 | 58 | Date getCalendarDateEnd(String agencyId); 59 | 60 | Date getCalendarServiceRangeStart(String agencyId); 61 | 62 | Date getCalendarServiceRangeEnd(String agencyId); 63 | 64 | Rectangle2D getBounds (); 65 | 66 | Statistic getStatistic(String agencyId); 67 | } 68 | -------------------------------------------------------------------------------- /gtfs-validator-json/src/main/java/com/conveyal/gtfs/validator/json/serialization/Rectangle2DDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator.json.serialization; 2 | 3 | import java.awt.geom.Rectangle2D; 4 | import java.io.IOException; 5 | 6 | import com.fasterxml.jackson.core.JsonParseException; 7 | import com.fasterxml.jackson.core.JsonParser; 8 | import com.fasterxml.jackson.core.JsonProcessingException; 9 | import com.fasterxml.jackson.databind.DeserializationContext; 10 | import com.fasterxml.jackson.databind.JsonDeserializer; 11 | 12 | /** 13 | * From https://github.com/conveyal/gtfs-data-manager/blob/master/app/controllers/api/Rectangle2DDeserializer.java 14 | */ 15 | public class Rectangle2DDeserializer extends JsonDeserializer { 16 | 17 | @Override 18 | public Rectangle2D deserialize(JsonParser jp, DeserializationContext arg1) 19 | throws IOException, JsonProcessingException { 20 | 21 | IntermediateBoundingBox bbox = jp.readValueAs(IntermediateBoundingBox.class); 22 | 23 | if (bbox.north == null || bbox.south == null || bbox.east == null || bbox.west == null) 24 | throw new JsonParseException("Unable to deserialize bounding box; need north, south, east, and west.", jp.getCurrentLocation()); 25 | 26 | Rectangle2D.Double ret = new Rectangle2D.Double(bbox.west, bbox.north, 0, 0); 27 | ret.add(bbox.east, bbox.south); 28 | return ret; 29 | } 30 | 31 | /** 32 | * A place to hold information from the JSON stream temporarily. 33 | */ 34 | private static class IntermediateBoundingBox { 35 | public Double north; 36 | public Double south; 37 | public Double east; 38 | public Double west; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/java/com/conveyal/gtfs/GeoUtilsTests.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import org.junit.Test; 6 | 7 | import com.conveyal.gtfs.model.ProjectedCoordinate; 8 | import com.conveyal.gtfs.service.GeoUtils; 9 | import com.vividsolutions.jts.geom.Coordinate; 10 | import com.vividsolutions.jts.geom.Geometry; 11 | 12 | import junit.framework.Assert; 13 | 14 | public class GeoUtilsTests { 15 | 16 | @Test 17 | public void testProjectionConversion() { 18 | 19 | // lat/lon coords 20 | // note that they are reversed 21 | Coordinate coord1 = new Coordinate(47.604201,-122.311123); 22 | Coordinate coord2 = new Coordinate(47.565297, -122.300823); 23 | 24 | ProjectedCoordinate projCoord1 = GeoUtils.convertLatLonToEuclidean(coord1); 25 | ProjectedCoordinate projCoord2 = GeoUtils.convertLatLonToEuclidean(coord2); 26 | 27 | // XY are reversed too 28 | // This should be the x coord 29 | assertEquals(551778.8, projCoord1.y, 1.0); 30 | assertEquals(5272540.2, projCoord1.x, 1.0); 31 | 32 | Double distance = projCoord1.distance(projCoord2); 33 | 34 | assertEquals(distance, 4392.666986979272,1.0); 35 | 36 | coord1 = new Coordinate(38.925922, -77.044788); 37 | coord2 = new Coordinate(38.891726, -76.999470); 38 | 39 | projCoord1 = GeoUtils.convertLatLonToEuclidean(coord1); 40 | projCoord2 = GeoUtils.convertLatLonToEuclidean(coord2); 41 | 42 | distance = projCoord1.distance(projCoord2); 43 | 44 | Assert.assertEquals(distance, 5464.52118132449); 45 | 46 | } 47 | @Test 48 | public void getGeometryFromCoordTest(){ 49 | Geometry g = GeoUtils.getGeometryFromCoordinate(47.604201, -122.311123); 50 | // transposed again... 51 | // System.out.println(g); 52 | assertEquals(551778.8, g.getCoordinate().y,1.0); 53 | assertEquals(5272540.2, g.getCoordinate().x,1.0); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/java/com/conveyal/gtfs/GtfsStatisticsServiceCalendarDatesTest.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertNotNull; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | 10 | import org.junit.BeforeClass; 11 | import org.junit.Test; 12 | import org.onebusaway.gtfs.impl.GtfsDaoImpl; 13 | import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; 14 | import org.onebusaway.gtfs.serialization.GtfsReader; 15 | 16 | import com.conveyal.gtfs.service.impl.GtfsStatisticsService; 17 | 18 | public class GtfsStatisticsServiceCalendarDatesTest { 19 | 20 | static GtfsRelationalDaoImpl store = null; 21 | static GtfsStatisticsService gtfsStats = null; 22 | 23 | @BeforeClass 24 | public static void setUp() throws Exception { 25 | 26 | store = new GtfsRelationalDaoImpl(); 27 | GtfsReader reader = new GtfsReader(); 28 | 29 | File gtfsFile = new File("src/test/resources/test_gtfs1.zip"); 30 | 31 | try { 32 | reader.setInputLocation(gtfsFile); 33 | } catch (IOException e) { 34 | e.printStackTrace(); 35 | } 36 | 37 | try { 38 | reader.setInputLocation(gtfsFile); 39 | } catch (IOException e1) { 40 | e1.printStackTrace(); 41 | } 42 | 43 | reader.setEntityStore(store); 44 | 45 | try { 46 | reader.run(); 47 | } catch (IOException e) { 48 | e.printStackTrace(); 49 | } 50 | 51 | gtfsStats = new GtfsStatisticsService(store); 52 | } 53 | 54 | @Test 55 | public void test() { 56 | assertNotNull(gtfsStats.getCalendarDateStart()); 57 | int numDays = gtfsStats.getNumberOfDays().intValue(); 58 | // Locally it's zero indexed, on Travis it's 1-indexed. Since it's a convenience method, I'm fudging. 59 | assertTrue(numDays == 28 || numDays ==29); 60 | } 61 | 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gtfs-validator 2 | ============== 3 | 4 | A Java framework for GTFS validation and statistics. 5 | 6 | [![Build Status](https://travis-ci.org/conveyal/gtfs-validator.svg?branch=master)](https://travis-ci.org/conveyal/gtfs-validator) 7 | 8 | How is this different than the Google-supported validator? 9 | ============= 10 | The Google TransitFeed-based [validator](https://github.com/google/transitfeed/blob/master/feedvalidator.py) is written in Python, and is quite slow on large feeds. 11 | 12 | This validator uses the [Onebusaway-GTFS](https://github.com/OneBusAway/onebusaway-gtfs-modules) library, written in Java and is far faster at processing large feeds. 13 | 14 | Downloads 15 | ============== 16 | Snapshots are available [here](build.staging.obanyc.com/archiva/repository/snapshots/com/conveyal/gtfs-validation-lib/) 17 | 18 | Using this framework 19 | ============== 20 | There are then multiple options for use: 21 | 22 | 1. Use the pre-built snapshot JAR. `java -server -Xmx4g -Xms3g -jar gtfs-validator.jar yourGtfs.zip` 23 | 24 | 2. Import the services provided and build your own validation. 25 | 26 | 3. Use the gtfs-validator-json and gtfs-validator-webapp according to the directions in those folders. 27 | 28 | ============== 29 | ValidatorMain 30 | 31 | The ValidatorMain class logs a number of common GTFS errors to Standard Out on the console when run as a JAR. This includes: 32 | 33 | * Problems with route names 34 | * Bad shape coordinates 35 | * Unused stops 36 | * Duplicate stops 37 | * Missing stop coordinates 38 | * Missing stop times for trips 39 | * Stop times out of sequence 40 | * Duplicated trips (same times) 41 | * Overlapping trips when block_id is present 42 | * Missing shapes and shape coordinates 43 | * “Reversed” shapes with directions that do not agree with stop times. 44 | * Exhaustively going through the calendar and printing active service IDs and number of trips for that day. 45 | * Dates with no active service 46 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/InvalidValue.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model; 2 | 3 | import java.io.Serializable; 4 | 5 | import org.onebusaway.gtfs.model.Route; 6 | 7 | public class InvalidValue implements Serializable, Comparable { 8 | 9 | /** 10 | * 11 | */ 12 | private static final long serialVersionUID = 1L; 13 | 14 | public String affectedEntity; 15 | 16 | public String affectedField; 17 | 18 | public String affectedEntityId; 19 | 20 | public String problemType; 21 | 22 | public String problemDescription; 23 | 24 | /** Is this something that is high priority or a nice-to-have? */ 25 | public Priority priority; 26 | 27 | public Object problemData; 28 | 29 | /** The route affected by this issue */ 30 | public Route route; 31 | 32 | @Deprecated 33 | /** 34 | * Create a new record of an invalid value. This function is deprecated in favor of the form that takes a priority as well. 35 | */ 36 | public InvalidValue(String affectedEntity, String affectedField, String affectedEntityId, String problemType, String problemDescription, Object problemData) { 37 | this(affectedEntity, affectedField, affectedEntityId, problemType, problemDescription, problemData, Priority.UNKNOWN); 38 | } 39 | 40 | public InvalidValue(String affectedEntity, String affectedField, String affectedEntityId, String problemType, 41 | String problemDescription, Object problemData, Priority priority) { 42 | this.affectedEntity = affectedEntity; 43 | this.affectedField = affectedField; 44 | this.affectedEntityId = affectedEntityId; 45 | this.problemType = problemType; 46 | this.problemDescription = problemDescription; 47 | this.problemData = problemData; 48 | this.priority = priority; 49 | } 50 | 51 | public String toString() { 52 | 53 | return problemType + "\t" + affectedEntityId + ":\t" + problemDescription; 54 | 55 | } 56 | 57 | @Override 58 | public int compareTo(Object o) { 59 | return this.toString().compareTo(o.toString()); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /gtfs-validator-json/src/main/java/com/conveyal/gtfs/validator/json/FeedValidationResult.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator.json; 2 | 3 | import java.awt.geom.Rectangle2D; 4 | import java.io.Serializable; 5 | import java.util.Collection; 6 | import java.util.Date; 7 | 8 | import com.conveyal.gtfs.model.ValidationResult; 9 | import com.fasterxml.jackson.annotation.JsonProperty; 10 | 11 | /** 12 | * A class to hold all of the results of a validation on a single feed. 13 | * Not to be confused with {@link com.conveyal.gtfs.model.ValidationResult}, which holds all instances of 14 | * a particular type of error. 15 | * @author mattwigway 16 | * 17 | */ 18 | public class FeedValidationResult implements Serializable { 19 | /** 20 | * 21 | */ 22 | private static final long serialVersionUID = 1L; 23 | 24 | /** Were we able to load the GTFS at all (note that this should only indicate corrupted files, 25 | * not missing ones; that should raise an exception instead.) 26 | */ 27 | @JsonProperty 28 | public LoadStatus loadStatus; 29 | 30 | /** 31 | * Additional description of why the feed failed to load. 32 | */ 33 | public String loadFailureReason; 34 | 35 | /** 36 | * The name of the feed on the file system 37 | */ 38 | public String feedFileName; 39 | 40 | /** 41 | * All of the agencies in the feed 42 | */ 43 | public Collection agencies; 44 | 45 | public ValidationResult routes; 46 | public ValidationResult stops; 47 | public ValidationResult trips; 48 | public ValidationResult shapes; 49 | 50 | // statistics 51 | public int agencyCount; 52 | public int routeCount; 53 | public int tripCount; 54 | public int stopTimesCount; 55 | 56 | /** The first date the feed has service, either in calendar.txt or calendar_dates.txt */ 57 | public Date startDate; 58 | 59 | /** The last date the feed has service, either in calendar.txt or calendar_dates.txt */ 60 | public Date endDate; 61 | 62 | /** The bounding box of the stops in this feed */ 63 | public Rectangle2D bounds; 64 | } -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/java/com/conveyal/gtfs/validator/ValidatorMainIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.io.IOException; 6 | import java.io.PrintStream; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | import java.util.TimeZone; 11 | import java.util.stream.Stream; 12 | 13 | import org.junit.BeforeClass; 14 | import org.junit.Test; 15 | 16 | import com.conveyal.gtfs.UnitTestBaseUtil; 17 | 18 | public class ValidatorMainIntegrationTest extends UnitTestBaseUtil { 19 | @BeforeClass 20 | public static void setUpClass(){ 21 | TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 22 | } 23 | 24 | private Stream getZipFiles() throws IOException { 25 | Path thisDir = Paths.get("src/test/resources"); 26 | 27 | if (!thisDir.toFile().exists()) 28 | thisDir = Paths.get("gtfs-validation-lib/src/test/resources"); 29 | 30 | if (!thisDir.toFile().exists()) { 31 | System.err.println("invalid working directory=" + Paths.get("")); 32 | } 33 | 34 | return Files.list(thisDir).filter(p -> p.getFileName().toString().endsWith(".zip")); 35 | } 36 | 37 | // @Test 38 | public void testProblem(){ 39 | 40 | // setDummyPrintStream(); 41 | 42 | ValidatorMain.main(new String[] {"src/test/resources/" 43 | + "st_gtfs_good" 44 | + ".zip"}); 45 | } 46 | 47 | @Test 48 | public void testAllGtfs() { 49 | System.out.println("Starting Integration Level Test on ValidatorMain (output suppressed)"); 50 | 51 | PrintStream originalStream = System.out; 52 | 53 | setDummyPrintStream(); 54 | 55 | try (Stream paths = getZipFiles()) { 56 | paths 57 | .filter(p -> !p.endsWith("gtfs_two_agencies.zip")) 58 | .filter(p -> !p.endsWith("20170119.zip")) 59 | .forEach(p -> ValidatorMain.main(new String[] {p.toString()})); 60 | } catch (IOException e) { 61 | e.printStackTrace(); 62 | fail(e.getMessage()); 63 | } catch (Exception e){ 64 | e.printStackTrace(); 65 | fail(e.getMessage()); 66 | } finally { 67 | System.setOut(originalStream); 68 | } 69 | } 70 | 71 | } 72 | 73 | -------------------------------------------------------------------------------- /gtfs-validator-json/src/main/java/com/conveyal/gtfs/validator/json/serialization/JsonSerializer.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator.json.serialization; 2 | 3 | import java.awt.geom.Rectangle2D; 4 | import java.io.File; 5 | import java.io.IOException; 6 | 7 | import com.conveyal.gtfs.validator.json.FeedValidationResultSet; 8 | import com.fasterxml.jackson.core.JsonGenerationException; 9 | import com.fasterxml.jackson.core.JsonProcessingException; 10 | import com.fasterxml.jackson.databind.JsonMappingException; 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | import com.fasterxml.jackson.databind.ObjectWriter; 13 | import com.fasterxml.jackson.databind.module.SimpleModule; 14 | import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; 15 | import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; 16 | 17 | /** 18 | * Serialize validation results to JSON, using Jackson 19 | * @author mattwigway 20 | */ 21 | public class JsonSerializer extends Serializer { 22 | private ObjectMapper mapper; 23 | private ObjectWriter writer; 24 | 25 | 26 | /** 27 | * Create a JSON serializer for these validation results. 28 | * @param results 29 | */ 30 | public JsonSerializer (FeedValidationResultSet results) { 31 | super(results); 32 | mapper = new ObjectMapper(); 33 | mapper.addMixInAnnotations(Rectangle2D.class, Rectangle2DMixIn.class); 34 | SimpleModule deser = new SimpleModule(); 35 | deser.addDeserializer(Rectangle2D.class, new Rectangle2DDeserializer()); 36 | mapper.registerModule(deser); 37 | SimpleFilterProvider filters = new SimpleFilterProvider(); 38 | filters.addFilter("bbox", SimpleBeanPropertyFilter.filterOutAllExcept("west", "east", "south", "north")); 39 | writer = mapper.writer(filters); 40 | } 41 | 42 | /** 43 | * Serialize to JSON 44 | * @return a string containing the serialized JSON 45 | */ 46 | public Object serialize() throws JsonProcessingException { 47 | return writer.writeValueAsString(results); 48 | } 49 | 50 | /** 51 | * Serialize to JSON and write to file. 52 | * @param file the file to write the JSON to 53 | */ 54 | public void serializeToFile(File file) throws JsonGenerationException, JsonMappingException, IOException { 55 | writer.writeValue(file, results); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/Statistic.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model; 2 | 3 | import java.awt.geom.Rectangle2D; 4 | import java.util.Date; 5 | 6 | /** 7 | * Model object representing statistics about GTFS. 8 | * 9 | */ 10 | public class Statistic { 11 | private String agencyId; 12 | private Integer routeCount; 13 | private Integer tripCount; 14 | private Integer stopCount; 15 | private Integer stopTimeCount; 16 | private Date calendarServiceStart; 17 | private Date calendarServiceEnd; 18 | private Date calendarStartDate; 19 | private Date calendarEndDate; 20 | private Rectangle2D bounds; 21 | 22 | public String getAgencyId() { 23 | return agencyId; 24 | } 25 | public void setAgencyId(String agencyId) { 26 | this.agencyId = agencyId; 27 | } 28 | public Integer getRouteCount() { 29 | return routeCount; 30 | } 31 | public void setRouteCount(Integer routeCount) { 32 | this.routeCount = routeCount; 33 | } 34 | public Integer getTripCount() { 35 | return tripCount; 36 | } 37 | public void setTripCount(Integer tripCount) { 38 | this.tripCount = tripCount; 39 | } 40 | public Integer getStopCount() { 41 | return stopCount; 42 | } 43 | public void setStopCount(Integer stopCount) { 44 | this.stopCount = stopCount; 45 | } 46 | public Integer getStopTimeCount() { 47 | return stopTimeCount; 48 | } 49 | public void setStopTimeCount(Integer stopTimeCount) { 50 | this.stopTimeCount = stopTimeCount; 51 | } 52 | public Date getCalendarStartDate() { 53 | return calendarStartDate; 54 | } 55 | public void setCalendarStartDate(Date calendarStartDate) { 56 | this.calendarStartDate = calendarStartDate; 57 | } 58 | public Date getCalendarEndDate() { 59 | return calendarEndDate; 60 | } 61 | public void setCalendarEndDate(Date calendarEndDate) { 62 | this.calendarEndDate = calendarEndDate; 63 | } 64 | public Date getCalendarServiceStart() { 65 | return calendarServiceStart; 66 | } 67 | public void setCalendarServiceStart(Date calendarServiceStart) { 68 | this.calendarServiceStart = calendarServiceStart; 69 | } 70 | public Date getCalendarServiceEnd() { 71 | return calendarServiceEnd; 72 | } 73 | public void setCalendarServiceEnd(Date calendarServiceEnd) { 74 | this.calendarServiceEnd = calendarServiceEnd; 75 | } 76 | public Rectangle2D getBounds() { 77 | return bounds; 78 | } 79 | public void setBounds(Rectangle2D bounds) { 80 | this.bounds = bounds; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /gtfs-validator-json/src/main/java/com/conveyal/gtfs/validator/json/JsonValidatorMain.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator.json; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | import com.conveyal.gtfs.validator.json.backends.FileSystemFeedBackend; 7 | import com.conveyal.gtfs.validator.json.serialization.JsonSerializer; 8 | 9 | public class JsonValidatorMain { 10 | 11 | /** 12 | * Take an input GTFS and an output file and write JSON to that output file summarizing validation of the GTFS. 13 | * @param args 14 | */ 15 | public static void main(String[] args) throws Exception { 16 | if (args.length < 2) { 17 | System.err.println("usage: java -Xmx[several]G input_gtfs.zip [other_gtfs.zip third_gtfs.zip . . .] output_file.json"); 18 | return; 19 | } 20 | 21 | // We use a file system backend because we're not doing anything fancy, just reading local GTFS 22 | FileSystemFeedBackend backend = new FileSystemFeedBackend(); 23 | 24 | // Since we're processing multiple feeds (potentially), use a FeedValidationResultSet to save the output 25 | FeedValidationResultSet results = new FeedValidationResultSet(); 26 | 27 | // default name is directory name 28 | results.name = new File(args[0]).getAbsoluteFile().getParentFile().getName(); 29 | 30 | // loop over all arguments except the last (which is the name of the JSON file) 31 | // TODO: throw all feeds in a queue and run as many threads as we have cores to do the validation 32 | // not exactly urgent, since for all of New York State this takes only a few minutes on my laptop 33 | for (int i = 0; i < args.length - 1; i++) { 34 | File input = backend.getFeed(args[i]); 35 | System.err.println("Processing feed " + input.getName()); 36 | FeedProcessor processor = new FeedProcessor(input); 37 | try { 38 | processor.run(); 39 | } catch (IOException e) { 40 | e.printStackTrace(); 41 | System.err.println("Unable to access input GTFS " + input.getPath() + ". Does the file exist and do I have permission to read it?"); 42 | return; 43 | } 44 | 45 | results.add(processor.getOutput()); 46 | } 47 | 48 | JsonSerializer serializer = new JsonSerializer(results); 49 | // TODO: error handling 50 | serializer.serializeToFile(new File(args[args.length - 1])); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /gtfs-validator-json/src/main/java/com/conveyal/gtfs/validator/json/FeedValidationResultSet.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator.json; 2 | import java.util.Date; 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | 8 | /** 9 | * Represents the results of a single validator run on multiple GTFS files. 10 | * @author mattwigway 11 | */ 12 | public class FeedValidationResultSet { 13 | /** The name of this feedset. In the simplest case this is the name of the directory 14 | * containing the first feed. 15 | */ 16 | public String name; 17 | 18 | /** 19 | * The date of this run 20 | */ 21 | public Date date; 22 | 23 | // note: the following three fields are private because applications should not manipulate them 24 | // directly, as they track internal state. We add a JsonProperty annotation to each so that it 25 | // is exported in the JSON. While this theoretically embeds one format into code that is generally 26 | // intended to be format-generic, it shouldn't be an issue because the annotation should simply have 27 | // no effect outside of Jackson. 28 | 29 | /** 30 | * The number of feeds validated. 31 | */ 32 | @JsonProperty 33 | private int feedCount; 34 | 35 | /** 36 | * The number of feeds that were able to be loaded (they may have had validation errors, 37 | * but nothing that makes them unusable, at least not from a technical standpoint). 38 | */ 39 | @JsonProperty 40 | private int loadCount; 41 | 42 | /** 43 | * The validation results of all of the feeds. 44 | */ 45 | @JsonProperty 46 | private Set results; 47 | 48 | /** 49 | * Add a feed validation result to this result set. 50 | */ 51 | public void add(FeedValidationResult results) { 52 | this.results.add(results); 53 | feedCount++; 54 | if (LoadStatus.SUCCESS.equals(results.loadStatus)) 55 | loadCount++; 56 | } 57 | 58 | /** 59 | * Create a new FeedValidationResultSet with the given initial capacity. 60 | * @param capacity initial capacity 61 | */ 62 | public FeedValidationResultSet (int capacity) { 63 | this.results = new HashSet(capacity); 64 | this.date = new Date(); 65 | } 66 | 67 | /** 68 | * Create a new FeedValidationResultSet with a reasonable default initial capacity. 69 | */ 70 | public FeedValidationResultSet () { 71 | this(16); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/java/com/conveyal/gtfs/GtfsStatisticsServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.Date; 6 | import java.util.TimeZone; 7 | 8 | import org.junit.Before; 9 | import org.junit.BeforeClass; 10 | import org.junit.Test; 11 | import org.onebusaway.gtfs.impl.GtfsDaoImpl; 12 | import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; 13 | import org.onebusaway.gtfs.serialization.GtfsReader; 14 | 15 | import com.conveyal.gtfs.service.impl.GtfsStatisticsService; 16 | 17 | import junit.framework.Assert; 18 | 19 | public class GtfsStatisticsServiceTest extends UnitTestBaseUtil { 20 | 21 | static GtfsRelationalDaoImpl store = null; 22 | static GtfsStatisticsService gtfsStats = null; 23 | 24 | @BeforeClass 25 | public static void setUpClass() { 26 | TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 27 | 28 | store = new GtfsRelationalDaoImpl(); 29 | GtfsReader reader = new GtfsReader(); 30 | 31 | File gtfsFile = new File("src/test/resources/st_gtfs_good.zip"); 32 | 33 | try { 34 | reader.setInputLocation(gtfsFile); 35 | } catch (IOException e) { 36 | e.printStackTrace(); 37 | } 38 | 39 | try { 40 | reader.setInputLocation(gtfsFile); 41 | } catch (IOException e1) { 42 | e1.printStackTrace(); 43 | } 44 | 45 | reader.setEntityStore(store); 46 | 47 | try { 48 | reader.run(); 49 | } catch (IOException e) { 50 | e.printStackTrace(); 51 | } 52 | 53 | gtfsStats = new GtfsStatisticsService(store); 54 | 55 | } 56 | 57 | @Before 58 | public void SetUp(){ 59 | setDummyPrintStream(); 60 | } 61 | 62 | @Test 63 | public void agencyCount() { 64 | 65 | Assert.assertEquals(gtfsStats.getAgencyCount(), new Integer(1)); 66 | } 67 | 68 | @Test 69 | public void routeCount() { 70 | Assert.assertEquals(gtfsStats.getRouteCount(), new Integer(16)); 71 | } 72 | 73 | @Test 74 | public void tripCount() { 75 | Assert.assertEquals(gtfsStats.getTripCount(), new Integer(559)); 76 | } 77 | 78 | @Test 79 | public void stopCount() { 80 | Assert.assertEquals(gtfsStats.getStopCount(), new Integer(93)); 81 | } 82 | 83 | @Test 84 | public void stopTimeCount() { 85 | Assert.assertEquals(gtfsStats.getStopTimesCount(), new Integer(7345)); 86 | } 87 | 88 | @Test 89 | public void calendarDateRangeStart() { 90 | Assert.assertEquals(gtfsStats.getCalendarDateStart().get(), new Date(1401062400000l)); 91 | } 92 | 93 | @Test 94 | public void calendarDateRangeEnd() { 95 | Assert.assertEquals(gtfsStats.getCalendarDateEnd().get(), new Date(1401062400000l)); 96 | } 97 | 98 | } 99 | 100 | -------------------------------------------------------------------------------- /gtfs-validator-json/src/test/java/com/conveyal/gtfs/validator/json/test/JsonOutputTest.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator.json.test; 2 | 3 | import com.conveyal.gtfs.validator.json.FeedProcessor; 4 | import com.conveyal.gtfs.validator.json.FeedValidationResultSet; 5 | import com.conveyal.gtfs.validator.json.backends.FileSystemFeedBackend; 6 | import com.conveyal.gtfs.validator.json.serialization.JsonSerializer; 7 | import org.junit.BeforeClass; 8 | import org.junit.Test; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.io.PrintStream; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | import java.util.TimeZone; 17 | import java.util.stream.Stream; 18 | 19 | import static org.junit.Assert.fail; 20 | 21 | public class JsonOutputTest { 22 | 23 | @BeforeClass 24 | public static void setUpClass() { 25 | TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 26 | } 27 | 28 | private Stream getZipFiles() throws IOException { 29 | Path thisDir = Paths.get("src/test/resources"); 30 | return Files.list(thisDir).filter(p -> p.getFileName().toString().endsWith(".zip")); 31 | } 32 | 33 | @Test 34 | public void testAllGtfs() { 35 | System.out.println("Starting JSON output test (output suppressed)"); 36 | 37 | PrintStream originalStream = System.out; 38 | 39 | try (Stream paths = getZipFiles()) { 40 | paths.forEach(p -> { 41 | try { 42 | outputJson(new String[]{p.toString()}); 43 | } catch (IOException e) { 44 | e.printStackTrace(); 45 | fail(e.getMessage()); 46 | } 47 | }); 48 | } catch (Exception e) { 49 | e.printStackTrace(); 50 | fail(e.getMessage()); 51 | } finally { 52 | System.setOut(originalStream); 53 | } 54 | } 55 | 56 | /** 57 | * Writes the results of validation to JSON for each provided GTFS dataset 58 | * 59 | * @param paths array of String paths to GTFS zip files 60 | * @throws IOException 61 | */ 62 | private void outputJson(String[] paths) throws IOException { 63 | for (String path : paths) { 64 | FileSystemFeedBackend backend = new FileSystemFeedBackend(); 65 | FeedValidationResultSet results = new FeedValidationResultSet(); 66 | File input = backend.getFeed(path); 67 | FeedProcessor processor = new FeedProcessor(input); 68 | processor.run(); 69 | results.add(processor.getOutput()); 70 | JsonSerializer serializer = new JsonSerializer(results); 71 | String saveFilePath = input.getName() + "_out.json"; 72 | serializer.serializeToFile(new File(saveFilePath)); 73 | } 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /gtfs-validation-lib/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | gtfs-validator 6 | com.conveyal 7 | 0.1.8-SNAPSHOT 8 | 9 | com.conveyal 10 | gtfs-validation-lib 11 | 0.1.8-SNAPSHOT 12 | A Java framework for GTFS validation and statistics 13 | 14 | 15 | UTF-8 16 | 17 | 18 | 19 | 20 | org.onebusaway 21 | onebusaway-gtfs 22 | 1.3.9 23 | 24 | 25 | junit 26 | junit 27 | 4.8.1 28 | test 29 | 30 | 31 | org.slf4j 32 | slf4j-simple 33 | 1.7.5 34 | 35 | 36 | 37 | org.geotools 38 | gt-api 39 | 15.1 40 | 41 | 42 | 43 | org.geotools 44 | gt-epsg-hsql 45 | 15.1 46 | 47 | 48 | org.geotools 49 | gt-jts-wrapper 50 | 15.1 51 | 52 | 53 | 54 | 55 | 56 | 57 | org.apache.maven.plugins 58 | maven-shade-plugin 59 | 2.2 60 | 61 | 62 | 63 | package 64 | 65 | shade 66 | 67 | 68 | gtfs-validator 69 | 70 | 71 | 72 | com.conveyal.gtfs.validator.ValidatorMain 73 | 74 | 75 | 76 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.conveyal 5 | gtfs-validator 6 | 0.1.8-SNAPSHOT 7 | pom 8 | 9 | 10 | 11 | releases.onebusaway.org 12 | http://nexus.onebusaway.org/content/repositories/releases/ 13 | true 14 | false 15 | 16 | 17 | osgeo 18 | Open Source Geospatial Foundation Repository 19 | http://download.osgeo.org/webdav/geotools/ 20 | 21 | 22 | releases.svn2.camsys.com 23 | http://svn2.camsys.com/archiva/repository/releases/ 24 | 25 | true 26 | 27 | 28 | false 29 | 30 | 31 | 32 | snapshots.svn2.camsys.com 33 | http://svn2.camsys.com/archiva/repository/snapshots/ 34 | 35 | false 36 | 37 | 38 | true 39 | 40 | 41 | 42 | 43 | 44 | scm:git:git@github.com:conveyal/gtfs-validator.git 45 | scm:git:git@github.com:conveyal/gtfs-validator.git 46 | scm:git:git@github.com:conveyal/gtfs-validator 47 | gtfs-validator-0.1.8-SNAPSHOT 48 | 49 | 50 | 51 | GitHub 52 | https://github.com/conveyal/gtfs-validator/issues 53 | 54 | 55 | 56 | 57 | nexus.onebusaway.org-releases 58 | http://nexus.onebusaway.org/content/repositories/releases/ 59 | 60 | 61 | nexus.onebusaway.org-snapshots 62 | http://nexus.onebusaway.org/content/repositories/snapshots/ 63 | 64 | 65 | 66 | 67 | gtfs-validation-lib 68 | gtfs-validator-json 69 | 70 | 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-compiler-plugin 76 | 3.1 77 | 78 | 1.8 79 | 1.8 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /gtfs-validator-json/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | gtfs-validator 5 | com.conveyal 6 | 0.1.8-SNAPSHOT 7 | 8 | com.conveyal 9 | gtfs-validator-json 10 | 0.1.8-SNAPSHOT 11 | gtfs-validator-json 12 | http://conveyal.com 13 | 14 | 15 | UTF-8 16 | 17 | 18 | 19 | 20 | junit 21 | junit 22 | 3.8.1 23 | test 24 | 25 | 26 | com.conveyal 27 | gtfs-validation-lib 28 | 0.1.8-SNAPSHOT 29 | 30 | 31 | org.onebusaway 32 | onebusaway-gtfs 33 | 1.3.3 34 | 35 | 36 | com.fasterxml.jackson.core 37 | jackson-core 38 | 2.3.1 39 | 40 | 41 | com.fasterxml.jackson.core 42 | jackson-databind 43 | 2.3.3 44 | 45 | 46 | junit 47 | junit 48 | 4.12 49 | 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-compiler-plugin 57 | 3.1 58 | 59 | 1.8 60 | 1.8 61 | 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-shade-plugin 67 | 2.2 68 | 69 | 70 | 71 | package 72 | shade 73 | 74 | gtfs-validator-json 75 | 76 | 77 | 78 | com.conveyal.gtfs.validator.json.JsonValidatorMain 79 | 80 | 81 | 82 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/model/TripPatternCollection.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.model; 2 | 3 | import java.util.LinkedHashSet; 4 | import java.util.List; 5 | import java.util.Set; 6 | 7 | import org.onebusaway.gtfs.model.AgencyAndId; 8 | import org.onebusaway.gtfs.model.Route; 9 | import org.onebusaway.gtfs.model.StopTime; 10 | /* 11 | * This is a model of convenience for holding onto metadata on trip patterns. 12 | * As GTFS does not model patterns explicitly, it uses the tuple 13 | * (route_id)-(shape_id)-(length(stopTimes)) 14 | * to distinguish between them. 15 | * 16 | * This model is intended for speedily checking if something that looks like the same pattern has been seen before. 17 | * 18 | * If this is too simplistic, then a List could be added in the future. 19 | * 20 | * from TCRP report 135, a trip/service pattern is: 21 | * 22 | * The unique sequence of stops associated with each type of trip on a route. 23 | * If all trips operate from one end to the other on a common path the route has one service pattern. 24 | * Branches, deviations, or short turns introduce additional service patterns. 25 | * Service patterns are a fundamental component of scheduling and provide the framework for 26 | * tracking running time, generating revenue trips, and identifying deadhead movements for the route. 27 | */ 28 | public class TripPatternCollection { 29 | private Set patterns; 30 | 31 | public TripPatternCollection(int estimatedSize){ 32 | patterns = new LinkedHashSet(estimatedSize); 33 | } 34 | 35 | public void add(Route routeId, AgencyAndId shapeId, List stops){ 36 | TripPattern tp = new TripPattern(routeId, shapeId, stops.size()); 37 | patterns.add(tp); 38 | } 39 | 40 | public Boolean addIfNotPresent(Route routeId, AgencyAndId shapeId, List stops){ 41 | TripPattern tp = new TripPattern(routeId, shapeId, stops.size()); 42 | Boolean present = patterns.contains(tp); 43 | if (!present) { 44 | patterns.add(tp); 45 | } 46 | return present; 47 | } 48 | 49 | 50 | private class TripPattern{ 51 | private String routeId; 52 | private String shapeId; 53 | private int stopHash; 54 | 55 | private TripPattern(Route route, AgencyAndId shape, int stopHash) { 56 | super(); 57 | this.routeId = route.toString(); 58 | this.shapeId = shape.toString(); 59 | this.stopHash = stopHash; 60 | } 61 | 62 | @Override 63 | public int hashCode() { 64 | final int prime = 31; 65 | int result = 1; 66 | result = prime * result + getOuterType().hashCode(); 67 | result = prime * result + ((routeId == null) ? 0 : routeId.hashCode()); 68 | result = prime * result + ((shapeId == null) ? 0 : shapeId.hashCode()); 69 | result = prime * result + stopHash; 70 | return result; 71 | } 72 | 73 | @Override 74 | public boolean equals(Object obj) { 75 | if (this == obj) 76 | return true; 77 | if (obj == null) 78 | return false; 79 | if (getClass() != obj.getClass()) 80 | return false; 81 | TripPattern other = (TripPattern) obj; 82 | if (!getOuterType().equals(other.getOuterType())) 83 | return false; 84 | if (routeId == null) { 85 | if (other.routeId != null) 86 | return false; 87 | } else if (!routeId.equals(other.routeId)) 88 | return false; 89 | if (shapeId == null) { 90 | if (other.shapeId != null) 91 | return false; 92 | } else if (!shapeId.equals(other.shapeId)) 93 | return false; 94 | if (stopHash != other.stopHash) 95 | return false; 96 | return true; 97 | } 98 | 99 | private TripPatternCollection getOuterType() { 100 | return TripPatternCollection.this; 101 | } 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/service/ServiceIdHelper.java: -------------------------------------------------------------------------------- 1 | // largely copied from https://github.com/camsys/onebusaway-nyc/blob/master/onebusaway-nyc-transit-data-federation/src/main/java/org/onebusaway/nyc/transit_data_federation/bundle/tasks/stif/model/ServiceCode.java 2 | // Currently NYC specific, works with AC Transit and possibly other HASTUS schedule exports as well. 3 | // If modifying this class, please make sure the accompanying test passes. 4 | 5 | package com.conveyal.gtfs.service; 6 | import java.util.HashMap; 7 | 8 | import com.conveyal.gtfs.model.HumanReadableServiceID; 9 | 10 | public class ServiceIdHelper { 11 | 12 | public HumanReadableServiceID getHumanReadableCalendarFromServiceId(String id) { 13 | HumanReadableServiceID sid= new HumanReadableServiceID(); 14 | 15 | try { 16 | String[] serviceIdParts = id.split("_"); 17 | 18 | if(serviceIdParts.length != 3){ 19 | serviceIdParts = id.split("-"); 20 | if (serviceIdParts.length > 1){ 21 | sid.setServiceId(id); 22 | } 23 | } 24 | String[] serviceIdSubparts = serviceIdParts[2].split("-"); 25 | 26 | String[] depotParts = serviceIdParts[1].split("-"); 27 | 28 | sid.setDepot(depotParts[depotParts.length -1]); 29 | 30 | String pickCode = serviceIdSubparts[0].toUpperCase(); 31 | 32 | char pickCodeWithoutYear = pickCode.toCharArray()[0]; 33 | if(pickCodeWithoutYear <= 'G') { 34 | if (id.contains("Weekday")) { 35 | if (id.contains("SDon")) { 36 | sid.setServiceId("WEEKDAY_SCHOOL_OPEN"); 37 | } else { 38 | sid.setServiceId("WEEKDAY_SCHOOL_CLOSED"); 39 | } 40 | } else if (id.contains("Saturday")) { 41 | sid.setServiceId("SATURDAY"); 42 | } else if (id.contains("Sunday")) { 43 | sid.setServiceId("SUNDAY"); 44 | } else 45 | sid.setServiceId(null); 46 | } else { 47 | // holiday code 48 | sid.setServiceId(ServiceCode.serviceCodeForGtfsId.get(Character.toString(pickCodeWithoutYear)).name()); 49 | } 50 | if (id.contains("BM") || id.contains("b4")){ 51 | sid.appendToServiceId(" Next Day's Trips Starting Before Midnight");; 52 | } 53 | } catch (Exception e) { 54 | sid.setServiceId(id); 55 | } 56 | 57 | return sid; 58 | } 59 | private enum ServiceCode { 60 | WEEKDAY_SCHOOL_OPEN, 61 | WEEKDAY_SCHOOL_CLOSED, 62 | SATURDAY, 63 | SUNDAY, 64 | MLK, 65 | PRESIDENTS_DAY, 66 | MEMORIAL_DAY, 67 | GOOD_FRIDAY, 68 | LABOR_DAY, 69 | JULY_FOURTH, 70 | COLUMBUS_DAY, 71 | THANKSGIVING, 72 | DAY_AFTER_THANKSGIVING, 73 | CHRISTMAS_EVE, 74 | CHRISTMAS_DAY, 75 | CHRISTMAS_DAY_OBSERVED, 76 | CHRISTMAS_WEEK, 77 | NEW_YEARS_EVE, 78 | NEW_YEARS_DAY, 79 | NEW_YEARS_DAY_OBSERVED; 80 | 81 | static HashMap serviceCodeForGtfsId = new HashMap(); 82 | static HashMap letterCodeForServiceCode = new HashMap(); 83 | 84 | static { 85 | mapServiceCode("1", WEEKDAY_SCHOOL_OPEN); 86 | mapServiceCode("11", WEEKDAY_SCHOOL_CLOSED); 87 | mapServiceCode("2", SATURDAY); 88 | mapServiceCode("3", SUNDAY); 89 | mapServiceCode("H", MLK); 90 | mapServiceCode("I", PRESIDENTS_DAY); 91 | mapServiceCode("J", GOOD_FRIDAY); 92 | mapServiceCode("K", MEMORIAL_DAY); 93 | mapServiceCode("M", JULY_FOURTH); 94 | mapServiceCode("N", LABOR_DAY); 95 | mapServiceCode("O", COLUMBUS_DAY); 96 | mapServiceCode("R", THANKSGIVING); 97 | mapServiceCode("S", DAY_AFTER_THANKSGIVING); 98 | mapServiceCode("T", CHRISTMAS_EVE); 99 | mapServiceCode("U", CHRISTMAS_DAY); 100 | mapServiceCode("V", CHRISTMAS_DAY_OBSERVED); 101 | mapServiceCode("W", CHRISTMAS_WEEK); 102 | mapServiceCode("X", NEW_YEARS_EVE); 103 | mapServiceCode("Y", NEW_YEARS_DAY); 104 | mapServiceCode("Z", NEW_YEARS_DAY_OBSERVED); 105 | } 106 | 107 | private static void mapServiceCode(String string, ServiceCode serviceCode) { 108 | serviceCodeForGtfsId.put(string, serviceCode); 109 | if (Character.isLetter(string.charAt(0))) { 110 | letterCodeForServiceCode.put(serviceCode, string); 111 | } 112 | } 113 | 114 | 115 | // private boolean isHoliday() { 116 | // return !(this == WEEKDAY_SCHOOL_OPEN || this == WEEKDAY_SCHOOL_CLOSED 117 | // || this == SATURDAY || this == SUNDAY); 118 | // } 119 | }} 120 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/java/com/conveyal/gtfs/CalendarDateVerificationServiceLarge.java: -------------------------------------------------------------------------------- 1 | 2 | package com.conveyal.gtfs; 3 | 4 | 5 | import static org.junit.Assert.assertEquals; 6 | import static org.junit.Assert.assertNotNull; 7 | import static org.junit.Assert.assertTrue; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.util.ArrayList; 12 | import java.util.Calendar; 13 | import java.util.Date; 14 | import java.util.GregorianCalendar; 15 | import java.util.TimeZone; 16 | import java.util.TreeMap; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | import java.util.concurrent.atomic.AtomicInteger; 19 | 20 | import org.junit.Before; 21 | import org.junit.BeforeClass; 22 | import org.junit.Test; 23 | import org.onebusaway.gtfs.impl.GtfsDaoImpl; 24 | import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; 25 | import org.onebusaway.gtfs.model.AgencyAndId; 26 | import org.onebusaway.gtfs.serialization.GtfsReader; 27 | import com.conveyal.gtfs.service.CalendarDateVerificationService; 28 | import com.conveyal.gtfs.service.impl.GtfsStatisticsService; 29 | 30 | import junit.framework.Assert; 31 | 32 | public class CalendarDateVerificationServiceLarge extends UnitTestBaseUtil{ 33 | static GtfsRelationalDaoImpl gtfsMDao = null; 34 | static GtfsDaoImpl gtfsDao = null; 35 | static GtfsStatisticsService gtfsStats = null; 36 | static CalendarDateVerificationService cdvs = null; 37 | static ConcurrentHashMap tripCounts = null; 38 | static Date calStart = null; 39 | static Date calEnd = null; 40 | 41 | @BeforeClass 42 | public static void setUpClass() { 43 | GtfsReader reader = new GtfsReader(); 44 | gtfsMDao = new GtfsRelationalDaoImpl(); 45 | 46 | File gtfsFile = new File("src/test/resources/brooklyn-a6-small.zip"); 47 | 48 | try { 49 | reader.setInputLocation(gtfsFile); 50 | } catch (IOException e) { 51 | e.printStackTrace(); 52 | } 53 | 54 | reader.setEntityStore(gtfsMDao); 55 | 56 | try { 57 | reader.run(); 58 | } catch (IOException e) { 59 | e.printStackTrace(); 60 | } 61 | 62 | gtfsStats = new GtfsStatisticsService(gtfsMDao); 63 | cdvs = new CalendarDateVerificationService(gtfsMDao); 64 | 65 | tripCounts = cdvs.getTripCountsForAllServiceIDs(); 66 | calStart = gtfsStats.getCalendarServiceRangeStart(); 67 | calEnd = gtfsStats.getCalendarServiceRangeEnd(); 68 | } 69 | 70 | @Before 71 | public void SetUp(){ 72 | setDummyPrintStream(); 73 | } 74 | 75 | @Test 76 | public void countOfServiceIdsInCalendarAndCalendarDates(){ 77 | int serviceIdCount = tripCounts.size(); 78 | Assert.assertEquals(19, serviceIdCount); 79 | } 80 | 81 | @Test 82 | public void feedCalendarExtents(){ 83 | assertEquals("start incorrect", "Sun Jan 03 00:00:00 EST 2016", calStart.toString()); 84 | assertEquals("end incorrect", "Sat Apr 02 00:00:00 EDT 2016", calEnd.toString()); 85 | 86 | } 87 | 88 | 89 | //Test for a bug with OneBusAway regarding DST. 90 | // 91 | //@Test 92 | public void somethingIsUpWithMarch13(){ 93 | 94 | TreeMap tripCounts= cdvs.getTripCountForDates(); 95 | 96 | Date mar13d = new Date(1457856000000L); 97 | Calendar mar13 = new GregorianCalendar(); 98 | mar13.setTimeZone(TimeZone.getTimeZone("America/New_York")); 99 | mar13.setTime(mar13d); 100 | 101 | TreeMap> dates = cdvs.getServiceIdsForDates(); 102 | 103 | ArrayList serviceforMar13 = dates.get(mar13); 104 | 105 | assertTrue(serviceforMar13.size() > 0); 106 | 107 | String message = mar13.getTime().toString() + " is not present"; 108 | assertTrue(message, tripCounts.containsKey(mar13)); 109 | 110 | Integer mar13Trips = tripCounts.get(mar13); 111 | 112 | assertTrue("0 trips", mar13Trips > 0); 113 | 114 | } 115 | // also fails with DST bug. 116 | // @Test 117 | public void tripEveryDay(){ 118 | Calendar aDay = new GregorianCalendar(); 119 | aDay.setTime(calStart); 120 | 121 | TreeMap tripCountForDates = cdvs.getTripCountForDates(); 122 | 123 | while (aDay.getTime().compareTo(calEnd) < 0){ 124 | 125 | 126 | int todaysTrips = 0; 127 | try { 128 | todaysTrips = tripCountForDates.get(aDay.getTime()); 129 | } catch (Exception e) { 130 | String message = aDay.getTime().toString() + " not present"; 131 | assertTrue(message, false); 132 | } 133 | 134 | assertNotNull(todaysTrips); 135 | assertTrue(todaysTrips > 0); 136 | 137 | aDay.add(Calendar.DATE, 1); 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/java/com/conveyal/gtfs/CalendarDateVerificationServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs; 2 | 3 | 4 | import static org.junit.Assert.assertEquals; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.util.ArrayList; 10 | import java.util.Calendar; 11 | import java.util.Date; 12 | import java.util.GregorianCalendar; 13 | import java.util.HashMap; 14 | import java.util.Set; 15 | import java.util.TreeMap; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | import java.util.concurrent.atomic.AtomicInteger; 18 | 19 | import org.junit.Before; 20 | import org.junit.BeforeClass; 21 | import org.junit.Test; 22 | import org.onebusaway.gtfs.impl.GtfsDaoImpl; 23 | import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; 24 | import org.onebusaway.gtfs.model.AgencyAndId; 25 | import org.onebusaway.gtfs.model.calendar.ServiceDate; 26 | import org.onebusaway.gtfs.serialization.GtfsReader; 27 | import org.onebusaway.gtfs.services.GtfsMutableRelationalDao; 28 | 29 | import com.conveyal.gtfs.service.CalendarDateVerificationService; 30 | import com.conveyal.gtfs.service.impl.GtfsStatisticsService; 31 | 32 | import junit.framework.Assert; 33 | 34 | public class CalendarDateVerificationServiceTest extends UnitTestBaseUtil { 35 | static GtfsRelationalDaoImpl gtfsMDao = null; 36 | static GtfsDaoImpl gtfsDao = null; 37 | static GtfsStatisticsService gtfsStats = null; 38 | static CalendarDateVerificationService cdvs = null; 39 | static ConcurrentHashMap tripCounts = null; 40 | 41 | 42 | @BeforeClass 43 | public static void setUpClass() { 44 | System.out.println("GtfsStatisticsTest setup"); 45 | 46 | GtfsReader reader = new GtfsReader(); 47 | gtfsMDao = new GtfsRelationalDaoImpl(); 48 | 49 | File gtfsFile = new File("src/test/resources/nyc_gtfs_si.zip"); 50 | 51 | try { 52 | reader.setInputLocation(gtfsFile); 53 | } catch (IOException e) { 54 | e.printStackTrace(); 55 | } 56 | 57 | reader.setEntityStore(gtfsMDao); 58 | 59 | try { 60 | reader.run(); 61 | } catch (IOException e) { 62 | e.printStackTrace(); 63 | } 64 | 65 | gtfsStats = new GtfsStatisticsService(gtfsMDao); 66 | cdvs = new CalendarDateVerificationService(gtfsMDao); 67 | 68 | tripCounts = cdvs.getTripCountsForAllServiceIDs(); 69 | } 70 | @Before 71 | public void SetUp(){ 72 | setDummyPrintStream(); 73 | } 74 | 75 | @Test 76 | public void tripCountForServiceId(){ 77 | int sundayTrips = tripCounts.get(AgencyAndId.convertFromString("MTA NYCT_YU_A5-Sunday")).get(); 78 | Assert.assertEquals(sundayTrips,110); 79 | } 80 | @Test 81 | public void countOfServiceIdsInCalendarAndCalendarDates(){ 82 | int serviceIdCount = tripCounts.size(); 83 | Assert.assertEquals(9, serviceIdCount); 84 | } 85 | 86 | @Test 87 | public void tripCountForDateWithMultipleServiceIDs(){ 88 | Date d = new Date(1427947200000L); 89 | Calendar c = new GregorianCalendar(); 90 | c.setTime(d); 91 | 92 | int regWeekday = cdvs.getTripCountForDates().get(c); 93 | Assert.assertEquals(regWeekday, 191); 94 | } 95 | 96 | @Test 97 | public void tripCountOnHoliday(){ 98 | 99 | Date day = new Date(1428033600000L); 100 | Calendar d = new GregorianCalendar(); 101 | d.setTime(day); 102 | d.setTimeZone(cdvs.getTz()); 103 | 104 | TreeMap serviceMap = cdvs.getTripCountForDates(); 105 | assert(serviceMap.size() > 0); 106 | 107 | assertTrue(serviceMap.containsKey(d)); 108 | 109 | int goodFriday = serviceMap.get(d); 110 | assertEquals(goodFriday, 160); 111 | } 112 | 113 | @Test 114 | public void serviceIdsForDateWithMultipleServiceIDs(){ 115 | Date day = new Date(1427947200000L); 116 | Calendar d = new GregorianCalendar(); 117 | d.setTime(day); 118 | d.setTimeZone(cdvs.getTz()); 119 | 120 | ArrayList idsOnWeekday = cdvs.getServiceIdsForDates().get(d); 121 | idsOnWeekday.forEach(t -> System.out.println(t.getId())); 122 | Assert.assertTrue(idsOnWeekday.size() > 1); 123 | } 124 | @Test 125 | public void serviceCalendarforCalendarDate(){ 126 | Date d = new Date(1428033603000L); 127 | ServiceDate sd = new ServiceDate(d); 128 | Set calendars = CalendarDateVerificationService.getCalendarsForDate(sd); 129 | Assert.assertEquals("MTA NYCT_YU_J5-Weekday".trim(), calendars.toArray()[0].toString().trim()); 130 | } 131 | 132 | @Test 133 | public void timeZoneTest(){ 134 | String tz = cdvs.getTz().getID(); 135 | assertEquals("Time Zone not America/New_York", tz, "America/New_York"); 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/test/java/com/conveyal/gtfs/GtfsValidationServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | import org.junit.Before; 7 | import org.junit.BeforeClass; 8 | import org.junit.Test; 9 | import org.onebusaway.csv_entities.exceptions.MissingRequiredFieldException; 10 | import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; 11 | import org.onebusaway.gtfs.serialization.GtfsReader; 12 | 13 | import com.conveyal.gtfs.model.ValidationResult; 14 | import com.conveyal.gtfs.service.GtfsValidationService; 15 | 16 | import junit.framework.Assert; 17 | 18 | public class GtfsValidationServiceTest extends UnitTestBaseUtil { 19 | 20 | static GtfsRelationalDaoImpl gtfsStore1 = null; 21 | static GtfsRelationalDaoImpl gtfsStore2 = null; 22 | 23 | static GtfsValidationService gtfsValidation1 = null; 24 | static GtfsValidationService gtfsValidation2 = null; 25 | 26 | static MissingRequiredFieldException mrf = null; 27 | 28 | @BeforeClass 29 | public static void setUpClass() { 30 | System.out.println("GtfsStatisticsTest setup"); 31 | 32 | gtfsStore1 = new GtfsRelationalDaoImpl(); 33 | gtfsStore2 = new GtfsRelationalDaoImpl(); 34 | 35 | GtfsReader gtfsReader1 = new GtfsReader(); 36 | GtfsReader gtfsReader2 = new GtfsReader(); 37 | 38 | File gtfsFile1 = new File("src/test/resources/test_gtfs1.zip"); 39 | File gtfsFile2 = new File("src/test/resources/test_gtfs2.zip"); 40 | 41 | 42 | try { 43 | 44 | gtfsReader1.setInputLocation(gtfsFile1); 45 | gtfsReader2.setInputLocation(gtfsFile2); 46 | 47 | } catch (IOException e) { 48 | e.printStackTrace(); 49 | } 50 | 51 | gtfsReader1.setEntityStore(gtfsStore1); 52 | gtfsReader2.setEntityStore(gtfsStore2); 53 | 54 | try { 55 | gtfsReader1.run(); 56 | gtfsReader2.run(); 57 | } catch (Exception e) { 58 | e.printStackTrace(); 59 | } 60 | 61 | try { 62 | gtfsValidation1 = new GtfsValidationService(gtfsStore1); 63 | gtfsValidation2 = new GtfsValidationService(gtfsStore2); 64 | } catch (Exception e) { 65 | e.printStackTrace(); 66 | } 67 | 68 | } 69 | 70 | @Before 71 | public void SetUp(){ 72 | setDummyPrintStream(); 73 | } 74 | 75 | @Test 76 | public void validateRoutes() { 77 | ValidationResult result = gtfsValidation2.validateRoutes(); 78 | 79 | Assert.assertEquals(5, result.invalidValues.size()); 80 | 81 | } 82 | // Test originally did not pass as some trips got included twice. 83 | @Test 84 | public void validateTrips() { 85 | ValidationResult result = gtfsValidation2.validateTrips(); 86 | Assert.assertEquals(8,result.invalidValues.size()); 87 | } 88 | 89 | @Test 90 | public void duplicateStops() { 91 | ValidationResult result = new ValidationResult(); 92 | 93 | result = gtfsValidation1.duplicateStops(); 94 | Assert.assertEquals(result.invalidValues.size(), 0); 95 | 96 | 97 | // try duplicate stop test to confirm that stops within the buffer limit are found 98 | result = gtfsValidation1.duplicateStops(25.0); 99 | Assert.assertEquals(result.invalidValues.size(), 1); 100 | 101 | // try same test to confirm that buffers below the limit don't detect duplicates 102 | result = gtfsValidation1.duplicateStops(5.0); 103 | Assert.assertEquals(result.invalidValues.size(), 0); 104 | } 105 | 106 | 107 | @Test 108 | public void reversedTripShapes() { 109 | 110 | ValidationResult result = gtfsValidation1.listReversedTripShapes(); 111 | 112 | Assert.assertEquals(result.invalidValues.size(), 1); 113 | 114 | // try again with an unusually high distanceMultiplier value 115 | result = gtfsValidation1.listReversedTripShapes(50000.0); 116 | 117 | Assert.assertEquals(result.invalidValues.size(), 0); 118 | 119 | } 120 | 121 | @Test 122 | public void completeBadGtfsTest() { 123 | 124 | GtfsRelationalDaoImpl gtfsStore = new GtfsRelationalDaoImpl(); 125 | 126 | GtfsReader gtfsReader = new GtfsReader(); 127 | 128 | File gtfsFile = new File("src/test/resources/st_gtfs_bad.zip"); 129 | 130 | try { 131 | 132 | gtfsReader.setInputLocation(gtfsFile); 133 | 134 | } catch (IOException e) { 135 | e.printStackTrace(); 136 | } 137 | 138 | gtfsReader.setEntityStore(gtfsStore); 139 | 140 | 141 | try { 142 | gtfsReader.run(); 143 | } catch (Exception e) { 144 | e.printStackTrace(); 145 | } 146 | 147 | try { 148 | GtfsValidationService gtfsValidation = new GtfsValidationService(gtfsStore); 149 | 150 | ValidationResult results = gtfsValidation.validateRoutes(); 151 | results.append(gtfsValidation.validateTrips()); 152 | 153 | Assert.assertEquals(results.invalidValues.size(), 5); 154 | 155 | } catch (Exception e) { 156 | e.printStackTrace(); 157 | } 158 | 159 | } 160 | 161 | @Test 162 | public void completeGoodGtfsTest() { 163 | 164 | GtfsRelationalDaoImpl gtfsStore = new GtfsRelationalDaoImpl(); 165 | GtfsReader gtfsReader = new GtfsReader(); 166 | 167 | File gtfsFile = new File("src/test/resources/st_gtfs_good.zip"); 168 | 169 | try { 170 | 171 | gtfsReader.setInputLocation(gtfsFile); 172 | 173 | } catch (IOException e) { 174 | e.printStackTrace(); 175 | } 176 | 177 | gtfsReader.setEntityStore(gtfsStore); 178 | 179 | 180 | try { 181 | gtfsReader.run(); 182 | } catch (Exception e) { 183 | e.printStackTrace(); 184 | } 185 | 186 | try { 187 | GtfsValidationService gtfsValidation = new GtfsValidationService(gtfsStore); 188 | 189 | ValidationResult results = gtfsValidation.validateRoutes(); 190 | results.append(gtfsValidation.validateTrips()); 191 | 192 | Assert.assertEquals(results.invalidValues.size(), 0); 193 | 194 | } catch (Exception e) { 195 | e.printStackTrace(); 196 | } 197 | 198 | } 199 | 200 | 201 | 202 | } 203 | -------------------------------------------------------------------------------- /gtfs-validator-json/src/main/java/com/conveyal/gtfs/validator/json/FeedProcessor.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator.json; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.*; 6 | import java.util.logging.Logger; 7 | import java.util.zip.ZipException; 8 | 9 | import org.onebusaway.csv_entities.exceptions.CsvEntityIOException; 10 | import org.onebusaway.csv_entities.exceptions.MissingRequiredFieldException; 11 | import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; 12 | import org.onebusaway.gtfs.model.Agency; 13 | import org.onebusaway.gtfs.serialization.GtfsReader; 14 | 15 | import com.conveyal.gtfs.model.InvalidValue; 16 | import com.conveyal.gtfs.service.GtfsValidationService; 17 | import com.conveyal.gtfs.service.StatisticsService; 18 | import com.conveyal.gtfs.service.impl.GtfsStatisticsService; 19 | 20 | /** 21 | * Process a feed and return the validation results and the statistics. 22 | * @author mattwigway 23 | */ 24 | public class FeedProcessor { 25 | private File feed; 26 | private GtfsRelationalDaoImpl dao; 27 | private FeedValidationResult output; 28 | private static Logger _log = Logger.getLogger(FeedProcessor.class.getName()); 29 | 30 | /** 31 | * Create a feed processor for the given feed 32 | * @param feed 33 | */ 34 | public FeedProcessor (File feed) { 35 | this.feed = feed; 36 | this.output = new FeedValidationResult(); 37 | } 38 | 39 | /** 40 | * Load the feed and run the validator and calculate statistics. 41 | * @throws IOException 42 | */ 43 | public void run () throws IOException { 44 | load(); 45 | if (output.loadStatus.equals(LoadStatus.SUCCESS)) { 46 | validate(); 47 | calculateStats(); 48 | } 49 | } 50 | 51 | /** 52 | * Load the feed into memory for processing. This is generally called from {@link #run}. 53 | * @throws IOException 54 | */ 55 | public void load () throws IOException { 56 | _log.fine("Loading GTFS"); 57 | 58 | // check if the file is accessible 59 | if (!feed.exists() || !feed.canRead()) 60 | throw new IOException("File does not exist or not readable"); 61 | 62 | output.feedFileName = feed.getName(); 63 | 64 | // note: we have two references because a GtfsDao is not mutable and we can't load to it, 65 | // but a GtfsDaoImpl is. 66 | GtfsRelationalDaoImpl dao = new GtfsRelationalDaoImpl(); 67 | this.dao = dao; 68 | GtfsReader reader = new GtfsReader(); 69 | reader.setEntityStore(dao); 70 | // Exceptions here mean a problem with the file 71 | try { 72 | reader.setInputLocation(feed); 73 | reader.run(); 74 | output.loadStatus = LoadStatus.SUCCESS; 75 | } 76 | catch (ZipException e) { 77 | output.loadStatus = LoadStatus.INVALID_ZIP_FILE; 78 | output.loadFailureReason = "Invalid ZIP file, not a ZIP file, or file corrupted"; 79 | } 80 | catch (CsvEntityIOException e) { 81 | Throwable cause = e.getCause(); 82 | if (cause instanceof MissingRequiredFieldException) { 83 | output.loadStatus = LoadStatus.MISSING_REQUIRED_FIELD; 84 | output.loadFailureReason = cause.getMessage(); 85 | } 86 | else if (cause instanceof IndexOutOfBoundsException) { 87 | output.loadStatus = LoadStatus.INCORRECT_FIELD_COUNT_IMPROPER_QUOTING; 88 | output.loadFailureReason = e.getMessage() + " (perhaps improper quoting)"; 89 | } 90 | 91 | else { 92 | output.loadStatus = LoadStatus.OTHER_FAILURE; 93 | output.loadFailureReason = "Unknown failure"; 94 | } 95 | } 96 | catch (IOException e) { 97 | output.loadStatus = LoadStatus.OTHER_FAILURE; 98 | } 99 | } 100 | 101 | /** 102 | * Run the GTFS validator 103 | */ 104 | public void validate () { 105 | GtfsValidationService validator = new GtfsValidationService(dao); 106 | 107 | _log.fine("Validating routes"); 108 | output.routes = validator.validateRoutes(); 109 | _log.fine("Validating trips"); 110 | output.trips = validator.validateTrips(); 111 | _log.fine("Finding duplicate stops"); 112 | output.stops = validator.duplicateStops(); 113 | _log.fine("Checking shapes"); 114 | output.shapes = validator.listReversedTripShapes(); 115 | 116 | // even though unused stops are found by validating trips, they make more sense as stop-level warnings 117 | // move them over 118 | Iterator tripIt = output.trips.invalidValues.iterator(); 119 | 120 | while (tripIt.hasNext()) { 121 | InvalidValue next = tripIt.next(); 122 | if (next.problemType.equals("UnusedStop")) { 123 | output.stops.invalidValues.add(next); 124 | tripIt.remove(); 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * Calculate statistics for the GTFS feed. 131 | */ 132 | public void calculateStats () { 133 | _log.fine("Calculating statistics"); 134 | 135 | StatisticsService stats = new GtfsStatisticsService(dao); 136 | 137 | Optional optionalCalDateStart = Optional.empty(); 138 | Optional optionalCalDateEnd = Optional.empty(); 139 | Date calDateStart = null; 140 | Date calDateEnd = null; 141 | 142 | output.agencyCount = stats.getAgencyCount(); 143 | output.routeCount = stats.getRouteCount(); 144 | output.tripCount = stats.getTripCount(); 145 | output.stopTimesCount = stats.getStopTimesCount(); 146 | output.bounds = stats.getBounds(); 147 | 148 | optionalCalDateStart = stats.getCalendarDateStart(); 149 | if(optionalCalDateStart.isPresent()) { 150 | calDateStart = optionalCalDateStart.get(); 151 | } 152 | Date calSvcStart = stats.getCalendarServiceRangeStart(); 153 | optionalCalDateEnd = stats.getCalendarDateEnd(); 154 | if(optionalCalDateEnd.isPresent()) { 155 | calDateEnd = optionalCalDateEnd.get(); 156 | } 157 | Date calSvcEnd = stats.getCalendarServiceRangeEnd(); 158 | 159 | if (calDateStart == null && calSvcStart == null) 160 | // no service . . . this is bad 161 | output.startDate = null; 162 | else if (calDateStart == null) 163 | output.startDate = calSvcStart; 164 | else if (calSvcStart == null) 165 | output.startDate = calDateStart; 166 | else 167 | output.startDate = calDateStart.before(calSvcStart) ? calDateStart : calSvcStart; 168 | 169 | if (calDateEnd == null && calSvcEnd == null) 170 | // no service . . . this is bad 171 | output.endDate = null; 172 | else if (calDateEnd == null) 173 | output.endDate = calSvcEnd; 174 | else if (calSvcEnd == null) 175 | output.endDate = calDateEnd; 176 | else 177 | output.endDate = calDateEnd.after(calSvcEnd) ? calDateEnd : calSvcEnd; 178 | 179 | Collection agencies = dao.getAllAgencies(); 180 | output.agencies = new HashSet(agencies.size()); 181 | for (Agency agency : agencies) { 182 | String agencyId = agency.getId(); 183 | output.agencies.add(agencyId == null || agencyId.isEmpty() ? agency.getName() : agencyId); 184 | } 185 | } 186 | 187 | public FeedValidationResult getOutput () { 188 | return output; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/service/GeoUtils.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.service; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.TreeSet; 6 | 7 | import org.geotools.geometry.GeometryBuilder; 8 | import org.geotools.geometry.jts.JTS; 9 | import org.geotools.referencing.CRS; 10 | import org.geotools.referencing.crs.DefaultGeographicCRS; 11 | import org.onebusaway.gtfs.model.ShapePoint; 12 | import org.opengis.referencing.FactoryException; 13 | import org.opengis.referencing.NoSuchIdentifierException; 14 | import org.opengis.referencing.crs.CRSAuthorityFactory; 15 | import org.opengis.referencing.crs.CoordinateReferenceSystem; 16 | import org.opengis.referencing.crs.GeographicCRS; 17 | import org.opengis.referencing.operation.MathTransform; 18 | import org.opengis.referencing.operation.TransformException; 19 | 20 | import com.conveyal.gtfs.model.ProjectedCoordinate; 21 | import com.vividsolutions.jts.geom.Coordinate; 22 | import com.vividsolutions.jts.geom.Geometry; 23 | import com.vividsolutions.jts.geom.GeometryFactory; 24 | import com.vividsolutions.jts.geom.PrecisionModel; 25 | 26 | public class GeoUtils { 27 | public static double RADIANS = 2 * Math.PI; 28 | 29 | public static MathTransform recentMathTransform = null; 30 | public static GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(),4326); 31 | public static GeometryFactory projectedGeometryFactory = new GeometryFactory(new PrecisionModel()); 32 | public static GeometryBuilder builder = new GeometryBuilder(DefaultGeographicCRS.WGS84); 33 | 34 | /** 35 | * Converts from a coordinate to The appropriate UTM zone. 36 | * 37 | * @param latlon THE ORDER OF THE COORDINATE MUST BE LAT, LON! 38 | * @return 39 | */ 40 | public static ProjectedCoordinate convertLatLonToEuclidean( 41 | Coordinate latlon) { 42 | 43 | Coordinate lonlat = new Coordinate(latlon.y, latlon.x); 44 | 45 | return convertLonLatToEuclidean(lonlat); 46 | } 47 | 48 | private static ProjectedCoordinate convertLonLatToEuclidean( 49 | Coordinate lonlat) { 50 | 51 | final MathTransform transform = getTransform(lonlat); 52 | final Coordinate to = new Coordinate(); 53 | 54 | // the transform seems to swap the lat lon pairs 55 | Coordinate latlon = new Coordinate(lonlat.y, lonlat.x); 56 | 57 | try { 58 | JTS.transform(latlon, to, transform); 59 | } catch (final TransformException e) { 60 | e.printStackTrace(); 61 | } 62 | 63 | return new ProjectedCoordinate(transform, new Coordinate(to.y, to.x), lonlat); 64 | } 65 | 66 | 67 | public static Coordinate convertToLatLon( 68 | MathTransform transform, Coordinate xy) { 69 | 70 | Coordinate lonlat = convertToLonLat(transform, xy); 71 | return new Coordinate(lonlat.y, lonlat.x); 72 | } 73 | 74 | public static Coordinate convertToLonLat( 75 | MathTransform transform, Coordinate xy) { 76 | final Coordinate to = new Coordinate(); 77 | final Coordinate yx = new Coordinate(xy.y, xy.x); 78 | try { 79 | JTS.transform(yx, to, transform.inverse()); 80 | } catch (final TransformException e) { 81 | e.printStackTrace(); 82 | } 83 | return new Coordinate(to.y, to.x); 84 | } 85 | 86 | public static Coordinate convertToLatLon(ProjectedCoordinate pc) { 87 | 88 | final Coordinate point = new Coordinate(pc.getX(), pc.getY()); 89 | return convertToLatLon(pc.getTransform(), point); 90 | } 91 | 92 | 93 | 94 | public static Coordinate convertToLonLat(ProjectedCoordinate pc) { 95 | 96 | final Coordinate point = new Coordinate(pc.getX(), pc.getY()); 97 | return convertToLonLat(pc.getTransform(), point); 98 | } 99 | 100 | public static Geometry getGeometryFromCoordinate(double lat, double lon) throws IllegalArgumentException{ 101 | Coordinate stopCoord = new Coordinate(lat, lon); 102 | ProjectedCoordinate projectedStopCoord = null; 103 | projectedStopCoord = GeoUtils.convertLatLonToEuclidean(stopCoord); 104 | return geometryFactory.createPoint(projectedStopCoord); 105 | } 106 | 107 | public static Geometry getGeomFromShapePoints(List shapePoints) throws IllegalArgumentException{ 108 | ArrayList shapeCoords = new ArrayList(); 109 | // needs to be mutable to sort. TreeSet impl is less verbose than initializing another List. 110 | TreeSet linkedShapePoints = new TreeSet(); 111 | try { 112 | linkedShapePoints.addAll(shapePoints); 113 | } catch (Exception e) { 114 | e.printStackTrace(); 115 | } 116 | 117 | for(ShapePoint shapePoint : linkedShapePoints) { 118 | Coordinate coord = new Coordinate(shapePoint.getLat(), shapePoint.getLon()); 119 | 120 | ProjectedCoordinate projectedCoord = GeoUtils.convertLatLonToEuclidean(coord); 121 | if ( projectedCoord.getX() == Coordinate.NULL_ORDINATE || 122 | projectedCoord.getY() == Coordinate.NULL_ORDINATE){ 123 | throw new IllegalArgumentException("Something is wrong with " + shapePoint.getId() + 124 | " on shape " + shapePoint.getShapeId()); 125 | } 126 | shapeCoords.add(projectedCoord); 127 | } 128 | 129 | Geometry geom = geometryFactory.createLineString( 130 | shapeCoords.toArray(new Coordinate[shapePoints.size()])); 131 | 132 | return geom; 133 | } 134 | 135 | /** 136 | * From 137 | * http://gis.stackexchange.com/questions/28986/geotoolkit-conversion-from 138 | * -lat-long-to-utm 139 | */ 140 | public static int getEPSGCodefromUTS(Coordinate refLonLat) { 141 | // define base EPSG code value of all UTM zones; 142 | int epsg_code = 32600; 143 | // add 100 for all zones in southern hemisphere 144 | if (refLonLat.y < 0) { 145 | epsg_code += 100; 146 | } 147 | // finally, add zone number to code 148 | epsg_code += getUTMZoneForLongitude(refLonLat.x); 149 | 150 | return epsg_code; 151 | } 152 | 153 | 154 | public static double getMetersInAngleDegrees( 155 | double distance) { 156 | return distance / (Math.PI / 180d) / 6378137d; 157 | } 158 | 159 | public static MathTransform getTransform( 160 | Coordinate refLatLon) { 161 | try { 162 | final CRSAuthorityFactory crsAuthorityFactory = 163 | CRS.getAuthorityFactory(false); 164 | 165 | 166 | final GeographicCRS geoCRS = 167 | crsAuthorityFactory.createGeographicCRS("EPSG:4326"); 168 | 169 | final CoordinateReferenceSystem dataCRS = 170 | crsAuthorityFactory 171 | .createCoordinateReferenceSystem("EPSG:" 172 | + getEPSGCodefromUTS(refLatLon)); //EPSG:32618 173 | 174 | final MathTransform transform = 175 | CRS.findMathTransform(geoCRS, dataCRS); 176 | 177 | GeoUtils.recentMathTransform = transform; 178 | 179 | return transform; 180 | } catch (final NoSuchIdentifierException e) { 181 | e.printStackTrace(); 182 | } catch (final FactoryException e) { 183 | e.printStackTrace(); 184 | } 185 | 186 | return null; 187 | } 188 | 189 | /* 190 | * Taken from OneBusAway's UTMLibrary class 191 | */ 192 | public static int getUTMZoneForLongitude(double lon) { 193 | 194 | if (lon < -180 || lon > 180) 195 | throw new IllegalArgumentException( 196 | "Coordinates not within UTM zone limits"); 197 | 198 | int lonZone = (int) ((lon + 180) / 6); 199 | 200 | if (lonZone == 60) 201 | lonZone--; 202 | return lonZone + 1; 203 | } 204 | 205 | 206 | 207 | 208 | } -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/validator/ValidatorMain.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.validator; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.ArrayList; 6 | import java.util.Date; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.logging.Handler; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | 13 | import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; 14 | import org.onebusaway.gtfs.model.Agency; 15 | import org.onebusaway.gtfs.serialization.GtfsReader; 16 | 17 | import com.conveyal.gtfs.model.InvalidValue; 18 | import com.conveyal.gtfs.model.ValidationResult; 19 | import com.conveyal.gtfs.service.CalendarDateVerificationService; 20 | import com.conveyal.gtfs.service.GtfsValidationService; 21 | import com.conveyal.gtfs.service.StatisticsService; 22 | import com.conveyal.gtfs.service.impl.GtfsStatisticsService; 23 | 24 | /** 25 | * Provides a main class for running the GTFS validator. 26 | * @author mattwigway 27 | * @author laidig 28 | */ 29 | public class ValidatorMain { 30 | 31 | public static String SILENT_MODE = "validate.silent"; 32 | 33 | public static void main(String[] args) { 34 | if (args.length != 1) { 35 | logError("Usage: gtfs-validator /path/to/gtfs.zip"); 36 | System.exit(-1); 37 | } 38 | 39 | // disable logging; we don't need logError messages from the validator printed to the console 40 | // Messages from inside OBA will still be printed, which is fine 41 | // loosely based upon http://stackoverflow.com/questions/470430 42 | for (Handler handler : Logger.getLogger("").getHandlers()) { 43 | handler.setLevel(Level.OFF); 44 | } 45 | 46 | File inputGtfs = new File(args[0]); 47 | 48 | logError("Reading GTFS from " + inputGtfs.getPath()); 49 | 50 | GtfsRelationalDaoImpl dao = new GtfsRelationalDaoImpl(); 51 | 52 | GtfsReader reader = new GtfsReader(); 53 | 54 | try { 55 | reader.setInputLocation(inputGtfs); 56 | reader.setEntityStore(dao); 57 | reader.run(); 58 | } catch (IOException e) { 59 | logError("Could not read file " + inputGtfs.getPath() + 60 | "; does it exist and is it readable?"); 61 | System.exit(-1); 62 | } 63 | 64 | logError("Read GTFS"); 65 | 66 | if (dao.getAllTrips().size() == 0){ 67 | logError("No Trips Found in GTFS, exiting"); 68 | System.exit(-1); 69 | } 70 | 71 | GtfsValidationService validationService = new GtfsValidationService(dao); 72 | 73 | CalendarDateVerificationService calendarDateVerService = new CalendarDateVerificationService(dao); 74 | 75 | logError("Validating routes"); 76 | ValidationResult routes = validationService.validateRoutes(); 77 | 78 | logError("Validating trips"); 79 | ValidationResult trips = validationService.validateTrips(); 80 | 81 | logError("Checking for duplicate stops"); 82 | ValidationResult stops = validationService.duplicateStops(); 83 | 84 | logError("Checking for problems with shapes"); 85 | ValidationResult shapes = validationService.listReversedTripShapes(); 86 | shapes.append(validationService.listStopsAwayFromShape(130.0)); 87 | 88 | logError("Checking for dates with no trips"); 89 | ValidationResult dates = calendarDateVerService.getCalendarProblems(); 90 | 91 | logError("Calculating statistics"); 92 | 93 | // Make the report 94 | StringBuilder sb = new StringBuilder(256); 95 | sb.append("# Validation report for "); 96 | 97 | List agencies = new ArrayList(dao.getAllAgencies()); 98 | int size = agencies.size(); 99 | 100 | for (int i = 0; i < size; i++) { 101 | sb.append(agencies.get(i).getName()); 102 | if (size - i == 1) { 103 | // append nothing, we're at the end 104 | } 105 | else if (size - i == 2) 106 | // the penultimate agency, use and 107 | // we can debate the relative merits of the Oxford comma at a later date, however not using has the 108 | // advantage that the two-agency case (e.g. BART and AirBART, comma would be gramatically incorrect) 109 | // is also handled. 110 | sb.append(" and "); 111 | else 112 | sb.append(", "); 113 | } 114 | 115 | log(sb.toString()); 116 | 117 | // generate and display feed statistics 118 | log("## Feed statistics"); 119 | StatisticsService stats = new GtfsStatisticsService(dao); 120 | 121 | log("- " + stats.getAgencyCount() + " agencies"); 122 | log("- " + stats.getRouteCount() + " routes"); 123 | log("- " + stats.getTripCount() + " trips"); 124 | log("- " + stats.getStopCount() + " stops"); 125 | log("- " + stats.getStopTimesCount() + " stop times"); 126 | 127 | Optional calDateStart = stats.getCalendarDateStart(); 128 | Date calSvcStart = stats.getCalendarServiceRangeStart(); 129 | Optional calDateEnd = stats.getCalendarDateEnd(); 130 | Date calSvcEnd = stats.getCalendarServiceRangeEnd(); 131 | 132 | Date feedSvcStart = getEarliestDate(calDateStart, calSvcStart); 133 | Date feedSvcEnd = getLatestDate(calDateEnd, calSvcEnd); 134 | 135 | // need an extra newline at the start so it doesn't get appended to the last list item if we let 136 | // a markdown processor loose on the output. 137 | log("\nFeed has service from " + feedSvcStart +" to " + feedSvcEnd); 138 | 139 | log("## Validation Results"); 140 | log("- Routes: " + getValidationSummary(routes)); 141 | log("- Trips: " + getValidationSummary(trips)); 142 | log("- Stops: " + getValidationSummary(stops)); 143 | log("- Shapes: " + getValidationSummary(shapes)); 144 | log("- Dates: " + getValidationSummary(dates)); 145 | 146 | log("\n### Routes"); 147 | log(getValidationReport(routes)); 148 | // no need for another line feed here to separate them, as one is added by getValidationReport and another by 149 | // log 150 | 151 | log("\n### Trips"); 152 | log(getValidationReport(trips)); 153 | 154 | log("\n### Stops"); 155 | log(getValidationReport(stops)); 156 | 157 | log("\n### Shapes"); 158 | log(getValidationReport(shapes)); 159 | 160 | log("\n### Dates"); 161 | log(getValidationReport(dates)); 162 | 163 | log("\n### Active Calendars"); 164 | log(calendarDateVerService.getTripDataForEveryDay()); 165 | } 166 | 167 | /** 168 | * Return a single-line summary of a ValidationResult 169 | */ 170 | public static String getValidationSummary(ValidationResult result) { 171 | return result.invalidValues.size() + " errors/warnings"; 172 | } 173 | 174 | /** 175 | * Return a human-readable, markdown-formatted multiline exhaustive report on a ValidationResult. 176 | */ 177 | public static String getValidationReport(ValidationResult result) { 178 | if (result.invalidValues.size() == 0) 179 | return "Hooray! No errors here (at least, none that we could find).\n"; 180 | 181 | StringBuilder sb = new StringBuilder(256); 182 | int i =0; 183 | int MAX_PRINT = 128; 184 | 185 | // loop over each invalid value, and take advantage of InvalidValue.toString to create a line about the error 186 | for (InvalidValue v : result.invalidValues) { 187 | i++; 188 | if (i > MAX_PRINT){ 189 | sb.append("And Many More..."); 190 | break; 191 | } 192 | sb.append("- "); 193 | sb.append(v.toString()); 194 | sb.append('\n'); 195 | 196 | } 197 | 198 | return sb.toString(); 199 | } 200 | 201 | static Date getEarliestDate(Optional o, Date d){ 202 | if (o.isPresent()){ 203 | d = o.get().before(d) ? o.get() : d; 204 | } 205 | return d; 206 | } 207 | 208 | static Date getLatestDate(Optional o, Date d){ 209 | if(o.isPresent()){ 210 | d = o.get().after(d) ? o.get(): d; 211 | } 212 | return d; 213 | } 214 | 215 | static void logError(String msg) { 216 | if ("true".equals(System.getProperty(SILENT_MODE))) { 217 | // no-op 218 | } else { 219 | System.err.println(msg); 220 | } 221 | } 222 | 223 | static void log(String msg) { 224 | if ("true".equals(System.getProperty(SILENT_MODE))) { 225 | // no-op 226 | } else { 227 | System.out.println(msg); 228 | } 229 | } 230 | 231 | } 232 | 233 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/service/CalendarDateVerificationService.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.service; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.Calendar; 7 | import java.util.Collection; 8 | import java.util.Collections; 9 | import java.util.Map; 10 | import java.util.Set; 11 | import java.util.TimeZone; 12 | import java.util.TreeMap; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; 16 | import org.onebusaway.gtfs.impl.calendar.CalendarServiceDataFactoryImpl; 17 | import org.onebusaway.gtfs.model.Agency; 18 | import org.onebusaway.gtfs.model.AgencyAndId; 19 | import org.onebusaway.gtfs.model.ServiceCalendarDate; 20 | import org.onebusaway.gtfs.model.calendar.ServiceDate; 21 | import org.onebusaway.gtfs.services.calendar.CalendarService; 22 | 23 | import com.conveyal.gtfs.model.InvalidValue; 24 | import com.conveyal.gtfs.model.Priority; 25 | import com.conveyal.gtfs.model.ValidationResult; 26 | import com.conveyal.gtfs.service.impl.GtfsStatisticsService; 27 | 28 | 29 | public class CalendarDateVerificationService { 30 | 31 | private static GtfsRelationalDaoImpl gtfsMDao = null; 32 | private static GtfsStatisticsService stats = null; 33 | private static CalendarService calendarService = null; 34 | private static Calendar start = null; 35 | private static Calendar end = null; 36 | private static TimeZone tz = null; 37 | private static ServiceDate from; 38 | private static ServiceDate to; 39 | private static String aid = null; 40 | 41 | public CalendarDateVerificationService(GtfsRelationalDaoImpl gmd){ 42 | gtfsMDao = gmd; 43 | stats = new GtfsStatisticsService(gmd); 44 | calendarService = CalendarServiceDataFactoryImpl.createService(gmd); 45 | 46 | start = Calendar.getInstance(); 47 | end = Calendar.getInstance(); 48 | 49 | from = new ServiceDate(stats.getCalendarServiceRangeStart()); 50 | to = new ServiceDate(stats.getCalendarServiceRangeEnd()); 51 | 52 | Collection agencies = stats.getAllAgencies(); 53 | 54 | Agency a = agencies.iterator().next(); 55 | 56 | //Do you know how many time zones there are in the Soviet Union? 57 | // if (agencies.size() == 1){ 58 | aid = a.getId(); 59 | // } 60 | // else { 61 | // for (Agency b: agencies){ 62 | // if (firstTz != b.getTimezone()){ 63 | // System.out.println(firstTz + b.getTimezone()); 64 | // System.err.println("Warning: This file may have two time zones"); 65 | // } 66 | // } 67 | // } 68 | 69 | tz = calendarService.getTimeZoneForAgencyId(aid); 70 | start.setTimeZone(tz); 71 | end.setTimeZone(tz); 72 | 73 | } 74 | public ConcurrentHashMap getTripCountsForAllServiceIDs() { 75 | // better way than this loop 76 | // for each route 77 | // geTripsPerRoute, then increment their calendarID counts. 78 | 79 | ConcurrentHashMap tripsPerCalHash = new ConcurrentHashMap(); 80 | gtfsMDao.getAllRoutes() 81 | .forEach(r -> gtfsMDao.getTripsForRoute(r) 82 | .forEach(t -> { 83 | tripsPerCalHash.putIfAbsent(t.getServiceId(), new AtomicInteger(0)); 84 | tripsPerCalHash.get(t.getServiceId()).incrementAndGet(); 85 | })); 86 | 87 | return tripsPerCalHash; 88 | } 89 | /* 90 | * @return a TreeMap (sorted by calendar) with the number of trips per day. 91 | */ 92 | public TreeMap getTripCountForDates() { 93 | 94 | ConcurrentHashMap tripsPerServHash = getTripCountsForAllServiceIDs(); 95 | TreeMap tripsPerDateHash = new TreeMap(); 96 | 97 | start.setTime(from.getAsDate(tz)); 98 | 99 | end.setTime(to.getAsDate(tz)); 100 | 101 | if (start == null){ 102 | throw new IllegalArgumentException("Calendar Date Range Improperly Set"); 103 | } 104 | 105 | while(!start.after(end)){ 106 | Integer tripCount =0; 107 | ServiceDate targetDay = new ServiceDate(start); 108 | Calendar targetDayAsCal = targetDay.getAsCalendar(tz); 109 | 110 | for (AgencyAndId sid : calendarService.getServiceIdsOnDate(targetDay)){ 111 | //System.out.println(targetDay.getAsCalendar(tz).getTime().toString() + " " +sid.toString()); 112 | if (tripsPerDateHash.containsKey(targetDayAsCal)){ 113 | tripCount = tripsPerDateHash.get(targetDayAsCal); 114 | } 115 | if (tripsPerServHash.containsKey(sid)){ 116 | tripCount = tripCount + tripsPerServHash.get(sid).get(); 117 | } 118 | } 119 | 120 | // System.out.println(targetDay.getAsCalendar(tz).getTime().toString() + " " + tripCount); 121 | 122 | tripsPerDateHash.put(targetDay.getAsCalendar(tz), tripCount); 123 | start.add(Calendar.DATE, 1); 124 | } 125 | 126 | return tripsPerDateHash; 127 | } 128 | 129 | public TreeMap> getServiceIdsForDates(){ 130 | TreeMap> serviceIdsForDates = new TreeMap>(); 131 | 132 | start.setTime(from.getAsDate(tz)); 133 | end.setTime(to.getAsDate(tz)); 134 | 135 | Collection allCalendarDates = gtfsMDao.getAllCalendarDates(); 136 | ConcurrentHashMap> dateAdditions = getCalendarDateAdditions(allCalendarDates); 137 | ConcurrentHashMap> dateRemovals = getCalendarDateRemovals(allCalendarDates); 138 | 139 | while(!start.after(end)){ 140 | 141 | ArrayList serviceIdsForTargetDay = new ArrayList(); 142 | 143 | ServiceDate targetDay = new ServiceDate(start); 144 | 145 | calendarService.getServiceIdsOnDate(targetDay).forEach(sid -> serviceIdsForTargetDay.add(sid)); 146 | 147 | dateAdditions.getOrDefault(targetDay, new ArrayList()).forEach(sid -> serviceIdsForTargetDay.add(sid)); 148 | 149 | dateRemovals.getOrDefault(targetDay, new ArrayList()).forEach(sid -> serviceIdsForTargetDay.remove(sid)); 150 | 151 | 152 | serviceIdsForDates.put(targetDay.getAsCalendar(tz), serviceIdsForTargetDay); 153 | start.add(Calendar.DATE, 1); 154 | } 155 | return serviceIdsForDates; 156 | 157 | } 158 | 159 | public ArrayList getDatesWithNoTrips(){ 160 | ArrayList datesWithNoTrips = new ArrayList(); 161 | TreeMap tc = getTripCountForDates(); 162 | for(Map.Entry d: tc.entrySet()){ 163 | if (d.getValue()==0){ 164 | datesWithNoTrips.add(d.getKey()); 165 | } 166 | } 167 | return datesWithNoTrips; 168 | } 169 | 170 | //I got 99 problems, and a calendar is one 171 | public ValidationResult getCalendarProblems(){ 172 | SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd"); 173 | 174 | ValidationResult vr = new ValidationResult(); 175 | ArrayList datesWithNoTrips = getDatesWithNoTrips(); 176 | for (Calendar d: datesWithNoTrips){ 177 | String dateFormatted = fmt.format(d.getTime()); 178 | InvalidValue iv = new InvalidValue("calendar", "service_id", dateFormatted, "NoServiceOnThisDate", 179 | "There is no service on " + dateFormatted, null, Priority.HIGH); 180 | vr.add(iv); 181 | } 182 | 183 | //TODO add checks for dates with significant decreases in service (e.g. missing depot) 184 | 185 | return vr; 186 | } 187 | 188 | public static Set getCalendarsForDate(ServiceDate date) { 189 | return calendarService.getServiceIdsOnDate(date); 190 | } 191 | 192 | public static String formatTripCountForServiceIDs(CalendarDateVerificationService t){ 193 | return Arrays.toString(t.getTripCountsForAllServiceIDs().entrySet().toArray()); 194 | } 195 | 196 | public String getTripDataForEveryDay(){ 197 | StringBuilder s = new StringBuilder(); 198 | ServiceIdHelper helper = new ServiceIdHelper(); 199 | SimpleDateFormat df = new SimpleDateFormat("E, yyyy-MM-dd"); 200 | Calendar yesterday = Calendar.getInstance(); 201 | yesterday.add(Calendar.DAY_OF_MONTH, -1);; 202 | 203 | TreeMap tc = getTripCountForDates(); 204 | for(Calendar d: tc.keySet()){ 205 | if (d.before(yesterday)){ 206 | continue; 207 | } 208 | s.append("\n#### " + df.format(d.getTime())); 209 | s.append("\n number of trips on this day: " + tc.get(d)); 210 | 211 | ArrayList aid = getServiceIdsForDates().get(d); 212 | Collections.sort(aid); 213 | for (AgencyAndId sid : aid){ 214 | s.append("\n" + helper.getHumanReadableCalendarFromServiceId(sid.toString())); 215 | } 216 | 217 | } 218 | return s.toString(); 219 | } 220 | public TimeZone getTz() { 221 | return tz; 222 | } 223 | public void setTz(TimeZone tz) { 224 | CalendarDateVerificationService.tz = tz; 225 | } 226 | 227 | private ConcurrentHashMap> getCalendarDateAdditions(Collection allCalendarDates){ 228 | ConcurrentHashMap> calDateMap = new ConcurrentHashMap<>(); 229 | allCalendarDates.stream().filter(d -> d.getExceptionType() ==1) 230 | .forEach(d -> { 231 | calDateMap.computeIfAbsent(d.getDate(), k-> new ArrayList()).add(d.getServiceId()); 232 | });; 233 | 234 | return calDateMap; 235 | } 236 | 237 | private ConcurrentHashMap> getCalendarDateRemovals(Collection allCalendarDates){ 238 | ConcurrentHashMap> calDateMap = new ConcurrentHashMap<>(); 239 | allCalendarDates.stream().filter(d -> d.getExceptionType() ==2) 240 | .forEach(d -> { 241 | calDateMap.computeIfAbsent(d.getDate(), k-> new ArrayList()).add(d.getServiceId()); 242 | });; 243 | 244 | return calDateMap; 245 | } 246 | 247 | 248 | 249 | } 250 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/service/impl/GtfsStatisticsService.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.gtfs.service.impl; 2 | 3 | import java.awt.geom.Point2D; 4 | import java.awt.geom.Rectangle2D; 5 | import java.time.Duration; 6 | import java.time.ZoneId; 7 | import java.util.Collection; 8 | import java.util.Date; 9 | import java.util.Optional; 10 | import java.util.TimeZone; 11 | 12 | import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; 13 | import org.onebusaway.gtfs.impl.calendar.CalendarServiceDataFactoryImpl; 14 | import org.onebusaway.gtfs.model.Agency; 15 | import org.onebusaway.gtfs.model.AgencyAndId; 16 | import org.onebusaway.gtfs.model.Route; 17 | import org.onebusaway.gtfs.model.ServiceCalendar; 18 | import org.onebusaway.gtfs.model.ServiceCalendarDate; 19 | import org.onebusaway.gtfs.model.Stop; 20 | import org.onebusaway.gtfs.model.StopTime; 21 | import org.onebusaway.gtfs.model.Trip; 22 | import org.onebusaway.gtfs.services.calendar.CalendarService; 23 | 24 | import com.conveyal.gtfs.model.Statistic; 25 | import com.conveyal.gtfs.service.StatisticsService; 26 | 27 | /** 28 | * Retrieves a base set of statistics from the GTFS. 29 | * 30 | */ 31 | public class GtfsStatisticsService implements StatisticsService { 32 | 33 | private GtfsRelationalDaoImpl gtfsDao = null; 34 | public GtfsStatisticsService(GtfsRelationalDaoImpl dao) { 35 | gtfsDao = dao; 36 | } 37 | 38 | public Integer getAgencyCount() { 39 | return gtfsDao.getAllAgencies().size(); 40 | } 41 | 42 | public Integer getRouteCount() { 43 | return gtfsDao.getAllRoutes().size(); 44 | } 45 | 46 | public Integer getTripCount() { 47 | return gtfsDao.getAllTrips().size(); 48 | } 49 | 50 | public Integer getStopCount() { 51 | return gtfsDao.getAllStops().size(); 52 | } 53 | 54 | public Integer getStopTimesCount() { 55 | return gtfsDao.getAllStopTimes().size(); 56 | } 57 | 58 | // calendar date range start/end assume a service calendar based schedule 59 | // returns null for schedules without calendar service schedules 60 | 61 | public Date getCalendarServiceRangeStart() { 62 | 63 | Date startDate = null; 64 | 65 | for (ServiceCalendar serviceCalendar : gtfsDao.getAllCalendars()) { 66 | 67 | if (startDate == null 68 | || serviceCalendar.getStartDate().getAsDate().before(startDate)) 69 | startDate = serviceCalendar.getStartDate().getAsDate(); 70 | } 71 | if (startDate != null){ 72 | return startDate; 73 | } else { 74 | // an exception here means that there are no dates in the feed at all 75 | return getCalendarDateStart().orElseThrow(IllegalStateException::new); 76 | } 77 | 78 | } 79 | 80 | public Date getCalendarServiceRangeEnd() { 81 | 82 | Date endDate = null; 83 | 84 | for (ServiceCalendar serviceCalendar : gtfsDao.getAllCalendars()) { 85 | if (endDate == null 86 | || serviceCalendar.getEndDate().getAsDate().after(endDate)) 87 | endDate = serviceCalendar.getEndDate().getAsDate(); 88 | } 89 | if (endDate != null){ 90 | return endDate; 91 | } else { 92 | return getCalendarDateEnd().orElseThrow(IllegalStateException::new); 93 | } 94 | } 95 | 96 | public Optional getCalendarDateStart() { 97 | 98 | Optional startDate = Optional.empty(); 99 | 100 | for (ServiceCalendarDate serviceCalendarDate : gtfsDao.getAllCalendarDates()) { 101 | 102 | if (!startDate.isPresent() 103 | || serviceCalendarDate.getDate().getAsDate().before(startDate.get())) 104 | startDate = Optional.of(serviceCalendarDate.getDate().getAsDate()); 105 | } 106 | 107 | return startDate; 108 | 109 | } 110 | 111 | public Optional getCalendarDateEnd() { 112 | 113 | Optional endDate = Optional.empty(); 114 | 115 | for (ServiceCalendarDate serviceCalendarDate : gtfsDao.getAllCalendarDates()) { 116 | 117 | if (!endDate.isPresent() 118 | || serviceCalendarDate.getDate().getAsDate().after(endDate.get())) 119 | endDate = Optional.of(serviceCalendarDate.getDate().getAsDate()); 120 | } 121 | 122 | return endDate; 123 | } 124 | 125 | public Collection getAllAgencies() { 126 | return gtfsDao.getAllAgencies(); 127 | } 128 | 129 | public Integer getRouteCount(String agencyId) { 130 | int count = 0; 131 | Collection routes = gtfsDao.getAllRoutes(); 132 | for (Route route : routes) { 133 | if (agencyId.equals(route.getAgency().getId())) { 134 | count++; 135 | } 136 | } 137 | return count; 138 | } 139 | 140 | public Integer getTripCount(String agencyId) { 141 | int count = 0; 142 | Collection trips = gtfsDao.getAllTrips(); 143 | for (Trip trip : trips) { 144 | if (agencyId.equals(trip.getRoute().getAgency().getId())) { 145 | count++; 146 | } 147 | } 148 | return count; 149 | } 150 | 151 | public Integer getStopCount(String agencyId) { 152 | int count = 0; 153 | Collection stops = gtfsDao.getAllStops(); 154 | for (Stop stop : stops) { 155 | AgencyAndId id = stop.getId(); 156 | if (agencyId.equals(id.getAgencyId())) { 157 | count++; 158 | } 159 | } 160 | return count; 161 | } 162 | 163 | public Integer getStopTimesCount(String agencyId) { 164 | int count = 0; 165 | Collection stopTimes = gtfsDao.getAllStopTimes(); 166 | for (StopTime stopTime : stopTimes) { 167 | if (agencyId.equals(stopTime.getTrip().getRoute().getAgency().getId())) { 168 | count++; 169 | } 170 | } 171 | return count; 172 | } 173 | 174 | public Date getCalendarServiceRangeStart(String agencyId) { 175 | 176 | Date startDate = null; 177 | 178 | for (ServiceCalendar serviceCalendar : gtfsDao.getAllCalendars()) { 179 | if (agencyId.equals(serviceCalendar.getServiceId().getAgencyId())) { 180 | if (startDate == null 181 | || serviceCalendar.getStartDate().getAsDate().before(startDate)) 182 | startDate = serviceCalendar.getStartDate().getAsDate(); 183 | } 184 | } 185 | 186 | return startDate; 187 | 188 | } 189 | 190 | public Date getCalendarServiceRangeEnd(String agencyId) { 191 | Date endDate = null; 192 | for (ServiceCalendar serviceCalendar : gtfsDao.getAllCalendars()) { 193 | if (agencyId.equals(serviceCalendar.getServiceId().getAgencyId())) { 194 | if (endDate == null 195 | || serviceCalendar.getEndDate().getAsDate().after(endDate)) 196 | endDate = serviceCalendar.getEndDate().getAsDate(); 197 | } 198 | } 199 | 200 | return endDate; 201 | } 202 | 203 | public Date getCalendarDateStart(String agencyId) { 204 | 205 | Date startDate = null; 206 | 207 | for (ServiceCalendarDate serviceCalendarDate : gtfsDao.getAllCalendarDates()) { 208 | if (agencyId.equals(serviceCalendarDate.getServiceId().getAgencyId())) { 209 | if (startDate == null 210 | || serviceCalendarDate.getDate().getAsDate().before(startDate)) 211 | startDate = serviceCalendarDate.getDate().getAsDate(); 212 | } 213 | } 214 | 215 | return startDate; 216 | 217 | } 218 | 219 | public Date getCalendarDateEnd(String agencyId) { 220 | Date endDate = null; 221 | for (ServiceCalendarDate serviceCalendarDate : gtfsDao.getAllCalendarDates()) { 222 | if (agencyId.equals(serviceCalendarDate.getServiceId().getAgencyId())) { 223 | if (endDate == null 224 | || serviceCalendarDate.getDate().getAsDate().after(endDate)) 225 | endDate = serviceCalendarDate.getDate().getAsDate(); 226 | } 227 | } 228 | 229 | return endDate; 230 | } 231 | 232 | /** 233 | * Get the bounding box of this GTFS feed. 234 | * We use a Rectangle2D rather than a Geotools envelope because GTFS is always in WGS 84. 235 | * Note that stops do not have agencies in GTFS. 236 | */ 237 | public Rectangle2D getBounds () { 238 | Rectangle2D ret = null; 239 | 240 | for (Stop stop : gtfsDao.getAllStops()) { 241 | if (ret == null) { 242 | ret = new Rectangle2D.Double(stop.getLon(), stop.getLat(), 0, 0); 243 | } 244 | else { 245 | ret.add(new Point2D.Double(stop.getLon(), stop.getLat())); 246 | } 247 | } 248 | 249 | return ret; 250 | } 251 | 252 | public Statistic getStatistic(String agencyId) { 253 | Statistic gs = new Statistic(); 254 | gs.setAgencyId(agencyId); 255 | gs.setRouteCount(getRouteCount(agencyId)); 256 | gs.setTripCount(getTripCount(agencyId)); 257 | gs.setStopCount(getStopCount(agencyId)); 258 | gs.setStopTimeCount(getStopTimesCount(agencyId)); 259 | gs.setCalendarStartDate(getCalendarDateStart(agencyId)); 260 | gs.setCalendarEndDate(getCalendarDateEnd(agencyId)); 261 | gs.setCalendarServiceStart(getCalendarServiceRangeStart(agencyId)); 262 | gs.setCalendarServiceEnd(getCalendarServiceRangeEnd(agencyId)); 263 | gs.setBounds(getBounds()); 264 | 265 | return gs; 266 | } 267 | 268 | public String getStatisticAsCSV(String agencyId) { 269 | Statistic s = getStatistic(agencyId); 270 | return formatStatisticAsCSV(s); 271 | 272 | } 273 | 274 | public static String formatStatisticAsCSV(Statistic s) { 275 | StringBuffer buff = new StringBuffer(); 276 | buff.append(s.getAgencyId()); 277 | buff.append(","); 278 | buff.append(s.getRouteCount()); 279 | buff.append(","); 280 | buff.append(s.getTripCount()); 281 | buff.append(","); 282 | buff.append(s.getStopCount()); 283 | buff.append(","); 284 | buff.append(s.getStopTimeCount()); 285 | buff.append(","); 286 | buff.append(s.getCalendarServiceStart()); 287 | buff.append(","); 288 | buff.append(s.getCalendarServiceEnd()); 289 | buff.append(","); 290 | buff.append(s.getCalendarStartDate()); 291 | buff.append(","); 292 | buff.append(s.getCalendarEndDate()); 293 | return buff.toString(); 294 | } 295 | 296 | private ZoneId getTimeZone(){ 297 | CalendarService calendarService = CalendarServiceDataFactoryImpl.createService(gtfsDao); 298 | TimeZone tz = calendarService.getTimeZoneForAgencyId(gtfsDao.getAllAgencies().iterator().next().getId()); 299 | return tz.toZoneId(); 300 | } 301 | /** 302 | * A convenience method primarily written for pre-allocating objects of a reasonable size. 303 | * Implementation result apparently varies based on JRE version. 304 | * Beware if used for validation 305 | */ 306 | @Deprecated 307 | @Override 308 | public Integer getNumberOfDays() { 309 | Duration d = Duration.between( 310 | getCalendarServiceRangeStart().toInstant().atZone(getTimeZone()).toInstant() 311 | , getCalendarServiceRangeEnd().toInstant().atZone(getTimeZone()).toInstant()); 312 | // zero indexed! 313 | return (int) d.toDays() +1; 314 | } 315 | 316 | } 317 | -------------------------------------------------------------------------------- /gtfs-validator-webapp/validation.js: -------------------------------------------------------------------------------- 1 | // validation.js: display the JSON from running the validator in a human-readable fashion 2 | 3 | var $ = require('jquery'); 4 | var Backbone = require('backbone'); 5 | Backbone.$ = $; 6 | var _ = require('underscore'); 7 | 8 | // framework for representing invalid values 9 | $(document).ready(function () { 10 | // Helpers for the views 11 | // hat tip: http://lostechies.com/derickbailey/2012/04/26/view-helpers-for-underscore-templates/ 12 | var viewHelpers = { 13 | // highlight the date appropriately for if it is within 2 weeks (yellow) or past (red) 14 | getClassForEndDate: function (date) { 15 | var daysToExpiration = (date - new Date()) / (60 * 60 * 24 * 1000); 16 | 17 | if (daysToExpiration > 14) { 18 | return ''; 19 | } 20 | else if (daysToExpiration >= 0) { 21 | return 'bg-warning'; 22 | } 23 | else return 'bg-danger'; 24 | }, 25 | 26 | getClassForStartDate: function (date) { 27 | if (new Date() - date >= 0) 28 | return ''; 29 | else return 'bg-danger'; 30 | }, 31 | 32 | getClassForSpan: function (startDate, endDate) { 33 | var daysToExpiration = (endDate - new Date()) / (60 * 60 * 24 * 1000); 34 | var daysSinceStart = (new Date() - startDate) / (60 * 60 * 24 * 1000); 35 | 36 | if (daysToExpiration < 0) { 37 | return 'bg-danger'; 38 | } 39 | else if (daysSinceStart < 0) { 40 | return 'bg-danger'; 41 | } 42 | else if (daysToExpiration < 14) { 43 | return 'bg-warning'; 44 | } 45 | else return ''; 46 | }, 47 | 48 | // bg-danger if the count is zero 49 | highlightZeroCount: function (count) { 50 | return count == 0 ? 'bg-danger' : ''; 51 | } 52 | }; 53 | 54 | var InvalidValue = Backbone.Model.extend({}); 55 | var InvalidValueColl = Backbone.Collection.extend({ 56 | model: InvalidValue, 57 | // we sort by status, but in a particular order; this way they get grouped with highest-priority items on top 58 | comparator: function (item) { 59 | return ['HIGH', 'MEDIUM', 'LOW', 'UNKNOWN'].indexOf(item.attributes.priority); 60 | } 61 | }); 62 | 63 | // template for showing invalid values 64 | var invalidValuesTemplate = _.template('<%- problemType %><%- affectedEntity %><%- affectedField %><%- problemDescription %>'); 65 | 66 | // template for a table/list of invalid values 67 | var invalidValuesListTemplate = require('./list.html'); 68 | 69 | // view for invalid values 70 | var InvalidValueView = Backbone.View.extend({ 71 | tagName: 'tr', 72 | className: 'invalid-value', 73 | render: function () { 74 | this.$el.html(invalidValuesTemplate(this.model.attributes)); 75 | return this; 76 | } 77 | }); 78 | 79 | // model with basic feed information as well as invalid value information specific to a feed 80 | var FeedModel = Backbone.Model.extend(); 81 | 82 | // template for basic information 83 | var feedTemplate = require('./feed.html'); 84 | 85 | // view for a header with basic information about a feed 86 | var FeedView = Backbone.View.extend({ 87 | tagName: 'div', 88 | // they start out hidden 89 | className: 'facet hidden', 90 | id: function () { return 'feed-' + this.model.attributes.index }, 91 | attributes: function () { return {"data-name": this.model.attributes.loadStatus == 'SUCCESS' ? this.model.attributes.agencies.join(', ') : this.model.attributes.feedFileName };}, 92 | render: function () { 93 | this.$el.html(feedTemplate(_.extend(this.model.attributes, viewHelpers))); 94 | 95 | // append the invalid value information 96 | // create the panels and populate them, but only if the load was successful (otherwise we have nothing to show) 97 | if (this.model.attributes.loadStatus == 'SUCCESS') { 98 | var content = this.$('.error-panel'); 99 | 100 | // the index is so that link hrefs remain unique 101 | new InvalidValueListView({collection: this.model.attributes.routes, model: new TypeModel({sing: 'Route', pl: 'Routes', index: this.model.attributes.index})}).render().$el.appendTo(content); 102 | new InvalidValueListView({collection: this.model.attributes.trips, model: new TypeModel({sing: 'Trip', pl: 'Trips', index: this.model.attributes.index})}).render().$el.appendTo(content); 103 | new InvalidValueListView({collection: this.model.attributes.stops, model: new TypeModel({sing: 'Stop', pl: 'Stops', index: this.model.attributes.index})}).render().$el.appendTo(content); 104 | new InvalidValueListView({collection: this.model.attributes.shapes, model: new TypeModel({sing: 'Shape', pl: 'Shapes', index: this.model.attributes.index})}).render().$el.appendTo(content); 105 | } 106 | 107 | return this; 108 | } 109 | }); 110 | 111 | // represents a type of error, with singular and plural human-readable forms 112 | var TypeModel = Backbone.Model.extend(); 113 | 114 | var InvalidValueListView = Backbone.View.extend({ 115 | tagName: 'div', 116 | className: 'panel panel-default', 117 | 118 | render: function () { 119 | this.$el.html(invalidValuesListTemplate({type: this.model.attributes.pl, errorCount: this.collection.length, index: this.model.attributes.index})); 120 | 121 | // populate the table 122 | // partition by error type to provide a more user friendly display 123 | var errorTypes = []; 124 | var errors = {}; 125 | 126 | // list is already sorted by error type 127 | this.collection.each(function (item) { 128 | if (errorTypes.indexOf(item.attributes.problemType) == -1) { 129 | errorTypes.push(item.attributes.problemType); 130 | errors[item.attributes.problemType] = new InvalidValueColl(); 131 | } 132 | 133 | errors[item.attributes.problemType].add(item); 134 | }); 135 | 136 | // render everything up 137 | for (var i = 0; i < errorTypes.length; i++) { 138 | new InvalidValueGroupView({collection: errors[errorTypes[i]]}) 139 | .render() 140 | .$el.appendTo(this.$('table')); 141 | } 142 | 143 | return this; 144 | }, 145 | }); 146 | 147 | // Not a list of errors (e.g. route errors) but a list of all errors for a specific type 148 | var InvalidValueGroupView = Backbone.View.extend({ 149 | tagName: 'tbody', // this results in multiple tbody elements, which is legal per MDN 150 | template: require('./group.html'), 151 | render: function () { 152 | this.$el.html(this.template(this.collection)); 153 | 154 | var instance = this; 155 | this.collection.each(function (item) { 156 | new InvalidValueView({model: item}).render().$el.appendTo(instance.$el); 157 | }); 158 | 159 | this.hidden = false; 160 | 161 | this.$('a.error-type').click(function (e) { 162 | e.preventDefault(); 163 | if (instance.hidden) { 164 | instance.$('.invalid-value').removeClass('hidden'); 165 | } 166 | else { 167 | instance.$('.invalid-value').addClass('hidden'); 168 | } 169 | 170 | instance.hidden = !instance.hidden; 171 | // we toggle a click to make it start out hidden 172 | }).click(); 173 | 174 | return this; 175 | } 176 | }); 177 | 178 | 179 | // a model representing what is open right now 180 | var NavModel = Backbone.Model.extend({ 181 | defaults: { 182 | current: null 183 | } 184 | }); 185 | 186 | // template for the breadcrumb navigation 187 | var navTemplate = require('./breadcrumb.html'); 188 | 189 | // this is an ugly workaroud: doNav needs to be called from within NavView, but also needs a reference to a NavView 190 | // so we define doNav here as a placeholder so it's in the closure, and then overwrite it below 191 | var doNav = null; 192 | 193 | // a view representing breadcrumb navigation for where we are right now 194 | var NavView = Backbone.View.extend({ 195 | tagName: 'ol', 196 | className: 'breadcrumb', 197 | render: function () { 198 | this.$el.html(navTemplate(this.model.attributes)); 199 | this.$('.jump').click(doNav); 200 | return this; 201 | } 202 | }); 203 | 204 | // these keep track of webapp state 205 | var navModel = new NavModel(); 206 | var navView = new NavView({model: navModel}); 207 | 208 | // navigates (in a section 508 friendly way) to the specified facet 209 | var doNav = function (e) { 210 | $('.facet').addClass('hidden'); 211 | var target = $($(this).attr('href')).removeClass('hidden').focus(); 212 | navModel.attributes.current = target; 213 | navView.render(); 214 | e.preventDefault(); 215 | } 216 | 217 | // represents an entire validation run 218 | var ValidationRunModel = Backbone.Model.extend(); 219 | 220 | var validationRunTemplate = require('./validationrun.html'); 221 | var feedTableEntryTemplate = require('./feedTable.html'); 222 | 223 | // displays an entire validation run 224 | var ValidationRunView = Backbone.View.extend({ 225 | el: '#content', 226 | render: function () { 227 | this.$el.html(validationRunTemplate(this.model.attributes)); 228 | 229 | // now attach the feed information 230 | var feedTable = this.$('.feed-table'); 231 | jQuery = $ = require('jquery'); 232 | this.collection.each(function (feed) { 233 | new FeedView({model: feed}).render().$el.appendTo(this.jQuery('.facets')); 234 | feedTable.append(feedTableEntryTemplate(_.extend(feed.attributes, viewHelpers))) 235 | }); 236 | 237 | navModel.attributes.current = this.$('#run'); 238 | navView.render().$el.appendTo(this.$('.feed-nav')); 239 | 240 | this.$('.jump').click(doNav); 241 | 242 | return this; 243 | } 244 | }); 245 | 246 | // represents an application error 247 | var ErrorModel = Backbone.Model.extend({ 248 | defaults: { 249 | title: 'Application error', 250 | message: '' 251 | } 252 | }); 253 | var errorTemplate = require('./error.html'); 254 | var ErrorView = Backbone.View.extend({ 255 | className: 'bg-danger error', 256 | 257 | render: function () { 258 | this.$el.html(errorTemplate(this.model.attributes)); 259 | return this; 260 | } 261 | }); 262 | 263 | // A collection of feed validation results 264 | var FeedColl = Backbone.Collection.extend({ 265 | comparator: function (feed) { 266 | return feed.attributes.loadStatus == 'SUCCESS' ? feed.attributes.agencies.join(', ') : feed.attributes.feedFileName; 267 | } 268 | }); 269 | 270 | // figure out what file we're pulling from 271 | // TODO: malformed search string handling 272 | var params = {}; 273 | // some browsers (I'm looking at you, Firefox) append a trailing slash after the query params 274 | if (location.search[location.search.length - 1] == '/') 275 | var search = location.search.slice(0, -1); 276 | else 277 | var search = location.search; 278 | var splitSearch = search.slice(1).split('&'); 279 | for (var i = 0; i < splitSearch.length; i++) { 280 | 281 | if (splitSearch[i].indexOf('=') == -1) { 282 | params[splitSearch[i]] = null; 283 | continue; 284 | } 285 | 286 | var splitParam = splitSearch[i].split('='); 287 | params[splitParam[0]] = decodeURIComponent(splitParam[1]); 288 | } 289 | 290 | if (params['report'] == undefined) { 291 | new ErrorView({ 292 | model: new ErrorModel({title: 'No report specified', message: 'Please specify a report to view'}) 293 | }).render().$el.appendTo('#content'); 294 | 295 | return; 296 | } 297 | 298 | // load the json and, when both it and the DOM are loaded, render it 299 | var routes, stops, trips, shapes; 300 | 301 | $.ajax({ 302 | url: params['report'], 303 | dataType: 'json', 304 | success: function (data) { 305 | var run = new ValidationRunModel({ 306 | name: data.name, 307 | date: new Date(data.date), 308 | feedCount: data.feedCount, 309 | loadCount: data.loadCount 310 | }); 311 | 312 | var feeds = new FeedColl(); 313 | 314 | var nfeeds = data.results.length; 315 | for (var i = 0; i < nfeeds; i++) { 316 | var feedData = data.results[i]; 317 | 318 | var routes, trips, stops, shapes; 319 | if (feedData.loadStatus == 'SUCCESS') { 320 | routes= new InvalidValueColl(feedData.routes.invalidValues); 321 | stops= new InvalidValueColl(feedData.stops.invalidValues); 322 | trips= new InvalidValueColl(feedData.trips.invalidValues); 323 | shapes= new InvalidValueColl(feedData.shapes.invalidValues); 324 | } 325 | else { 326 | routes = shapes = trips = stops = null; 327 | } 328 | 329 | feed = new FeedModel({ 330 | agencies: feedData.agencies, 331 | agencyCount: feedData.agencyCount, 332 | tripCount: feedData.tripCount, 333 | routeCount: feedData.routeCount, 334 | startDate: new Date(feedData.startDate), 335 | endDate: new Date(feedData.endDate), 336 | stopTimesCount: feedData.stopTimesCount, 337 | loadStatus: feedData.loadStatus, 338 | feedFileName: feedData.feedFileName, 339 | loadFailureReason: feedData.loadFailureReason, 340 | 341 | // just need a guaranteed-unique value attached to each feed for tabnav 342 | index: i, 343 | 344 | routes: routes, 345 | trips: trips, 346 | stops: stops, 347 | shapes: shapes, 348 | }); 349 | 350 | feeds.add(feed); 351 | } 352 | 353 | new ValidationRunView({model: run, collection: feeds}).render().$el.appendTo($('#content')); 354 | }, 355 | error: function () { 356 | new ErrorView({ 357 | model: new ErrorModel({title: 'Report could not be loaded', message: 'There was an error loading the report ' + params.report + '. Does it exist?'}) 358 | }).render().$el.appendTo('#content'); 359 | }, 360 | }); 361 | }); 362 | 363 | -------------------------------------------------------------------------------- /gtfs-validation-lib/src/main/java/com/conveyal/gtfs/service/GtfsValidationService.java: -------------------------------------------------------------------------------- 1 | 2 | package com.conveyal.gtfs.service; 3 | 4 | import java.util.ArrayList; 5 | import java.util.Calendar; 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.Date; 9 | import java.util.HashMap; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import java.util.Map.Entry; 13 | 14 | import org.onebusaway.gtfs.impl.GtfsRelationalDaoImpl; 15 | import org.onebusaway.gtfs.model.AgencyAndId; 16 | import org.onebusaway.gtfs.model.Route; 17 | import org.onebusaway.gtfs.model.ServiceCalendar; 18 | import org.onebusaway.gtfs.model.ServiceCalendarDate; 19 | import org.onebusaway.gtfs.model.ShapePoint; 20 | import org.onebusaway.gtfs.model.Stop; 21 | import org.onebusaway.gtfs.model.StopTime; 22 | import org.onebusaway.gtfs.model.Trip; 23 | 24 | import com.conveyal.gtfs.model.BlockInterval; 25 | import com.conveyal.gtfs.model.DuplicateStops; 26 | import com.conveyal.gtfs.model.InputOutOfRange; 27 | import com.conveyal.gtfs.model.InvalidValue; 28 | import com.conveyal.gtfs.model.Priority; 29 | import com.conveyal.gtfs.model.TripPatternCollection; 30 | import com.conveyal.gtfs.model.ValidationResult; 31 | import com.conveyal.gtfs.model.comparators.BlockIntervalComparator; 32 | import com.conveyal.gtfs.model.comparators.StopTimeComparator; 33 | import com.conveyal.gtfs.service.impl.GtfsStatisticsService; 34 | import com.vividsolutions.jts.geom.Coordinate; 35 | import com.vividsolutions.jts.geom.Geometry; 36 | import com.vividsolutions.jts.geom.GeometryFactory; 37 | import com.vividsolutions.jts.index.strtree.STRtree; 38 | 39 | public class GtfsValidationService { 40 | 41 | static GeometryFactory geometryFactory = new GeometryFactory(); 42 | 43 | private GtfsRelationalDaoImpl gtfsDao = null; 44 | private GtfsStatisticsService statsService = null; 45 | 46 | public GtfsValidationService(GtfsRelationalDaoImpl dao) { 47 | 48 | gtfsDao = dao; 49 | statsService = new GtfsStatisticsService(dao); 50 | } 51 | 52 | /** 53 | * Checks for invalid route values. Returns a ValidationResult object listing invalid/missing data. 54 | * 55 | */ 56 | public ValidationResult validateRoutes() { 57 | 58 | ValidationResult result = new ValidationResult(); 59 | 60 | for(Route route : gtfsDao.getAllRoutes()) { 61 | 62 | String routeId = route.getId().toString(); 63 | 64 | String shortName = ""; 65 | String longName = ""; 66 | String desc = ""; 67 | 68 | if(route.getShortName() != null) 69 | shortName = route.getShortName().trim().toLowerCase(); 70 | 71 | if(route.getLongName() != null) 72 | longName = route.getLongName().trim().toLowerCase(); 73 | 74 | if(route.getDesc() != null) 75 | desc = route.getDesc().toLowerCase(); 76 | 77 | 78 | //RouteShortAndLongNamesAreBlank 79 | if(longName.isEmpty() && shortName.isEmpty()) 80 | result.add(new InvalidValue("route", "route_short_name,route_long_name", routeId , "RouteShortAndLongNamesAreBlank", "", null, Priority.HIGH)); 81 | 82 | //ValidateRouteShortNameIsTooLong 83 | if(shortName.length() > 9) 84 | result.add(new InvalidValue("route", "route_short_name", routeId, "ValidateRouteShortNameIsTooLong", "route_short_name is " + shortName.length() + " chars ('" + shortName + "')" , null, Priority.MEDIUM)); 85 | 86 | //ValidateRouteLongNameContainShortName 87 | if(!longName.isEmpty() && !shortName.isEmpty() &&longName.contains(shortName)) 88 | result.add(new InvalidValue("route", "route_short_name,route_long_name", routeId, "ValidateRouteLongNameContainShortName", "'" + longName + "' contains '" + shortName + "'", null, Priority.MEDIUM)); 89 | 90 | //ValidateRouteDescriptionSameAsRouteName 91 | if(!desc.isEmpty() && (desc.equals(shortName) || desc.equals(longName))) 92 | result.add(new InvalidValue("route", "route_short_name,route_long_name,route_desc", routeId, "ValidateRouteDescriptionSameAsRouteName", "", null, Priority.MEDIUM)); 93 | 94 | //ValidateRouteTypeInvalidValid 95 | if(route.getType() < 0 || route.getType() > 7) 96 | result.add(new InvalidValue("route", "route_type", routeId, "ValidateRouteTypeInvalidValid", "route_type is " + route.getType(), null, Priority.HIGH)); 97 | 98 | } 99 | 100 | return result; 101 | 102 | } 103 | 104 | /** 105 | * Checks for invalid trip values. Returns a ValidationResult object listing invalid/missing data. 106 | * 107 | */ 108 | public ValidationResult validateTrips() { 109 | 110 | ValidationResult result = new ValidationResult(); 111 | 112 | 113 | // map stop time sequences to trip id 114 | 115 | HashMap> tripStopTimes = new HashMap>(statsService.getStopTimesCount() *2); 116 | 117 | HashSet usedStopIds = new HashSet(statsService.getStopCount() *2); 118 | 119 | String tripId; 120 | 121 | for(StopTime stopTime : gtfsDao.getAllStopTimes()) { 122 | 123 | tripId = stopTime.getTrip().getId().toString(); 124 | 125 | if(!tripStopTimes.containsKey(tripId)) 126 | tripStopTimes.put(tripId, new ArrayList()); 127 | 128 | tripStopTimes.get(tripId).add(stopTime); 129 | 130 | if (stopTime.getStop() != null && stopTime.getStop().getId() != null) { 131 | usedStopIds.add(stopTime.getStop().getId().toString()); 132 | } 133 | 134 | } 135 | 136 | // create service calendar date map 137 | 138 | 139 | @SuppressWarnings("deprecation") 140 | int reasonableNumberOfDates = statsService.getNumberOfDays() *2; 141 | 142 | HashMap> serviceCalendarDates = new HashMap>(reasonableNumberOfDates); 143 | //TODO: factor out. 144 | for(ServiceCalendar calendar : gtfsDao.getAllCalendars()) { 145 | 146 | Date startDate = calendar.getStartDate().getAsDate(); 147 | Date endDate = calendar.getEndDate().getAsDate(); 148 | 149 | HashSet datesActive = new HashSet(reasonableNumberOfDates); 150 | 151 | Date currentDate = startDate; 152 | 153 | HashSet daysActive = new HashSet(); 154 | 155 | if(calendar.getSunday() == 1) 156 | daysActive.add(Calendar.SUNDAY); 157 | else if(calendar.getMonday() == 1) 158 | daysActive.add(Calendar.MONDAY); 159 | else if(calendar.getTuesday() == 1) 160 | daysActive.add(Calendar.TUESDAY); 161 | else if(calendar.getWednesday() == 1) 162 | daysActive.add(Calendar.WEDNESDAY); 163 | else if(calendar.getThursday() == 1) 164 | daysActive.add(Calendar.THURSDAY); 165 | else if(calendar.getFriday() == 1) 166 | daysActive.add(Calendar.FRIDAY); 167 | else if(calendar.getSaturday() == 1) 168 | daysActive.add(Calendar.SATURDAY); 169 | 170 | while(currentDate.before(endDate) || currentDate.equals(endDate)) { 171 | 172 | Calendar cal = Calendar.getInstance(); 173 | cal.setTime(currentDate); 174 | 175 | if(daysActive.contains(cal.get(Calendar.DAY_OF_WEEK))) 176 | datesActive.add(currentDate); 177 | 178 | cal.add(Calendar.DATE, 1); 179 | currentDate = cal.getTime(); 180 | } 181 | 182 | serviceCalendarDates.put(calendar.getServiceId().getId(), datesActive); 183 | 184 | } 185 | 186 | // add/remove service exceptions 187 | for(ServiceCalendarDate calendarDate : gtfsDao.getAllCalendarDates()) { 188 | 189 | String serviceId = calendarDate.getServiceId().getId(); 190 | int exceptionType = calendarDate.getExceptionType(); 191 | 192 | if(serviceCalendarDates.containsKey(serviceId)) { 193 | 194 | if(exceptionType == 1) 195 | serviceCalendarDates.get(serviceId).add(calendarDate.getDate().getAsDate()); 196 | else if (exceptionType == 2 && serviceCalendarDates.get(serviceId).contains(calendarDate.getDate().getAsDate())) 197 | serviceCalendarDates.get(serviceId).remove(calendarDate.getDate().getAsDate()); 198 | } 199 | // handle service ids that don't appear in calendar.txt 200 | // for instance, feeds that have no calendar.txt (e.g. TriMet, NJ Transit) 201 | // and rely exclusively on calendar_dates.txt 202 | else if (exceptionType == 1) { 203 | HashSet calendarDates = new HashSet(); 204 | calendarDates.add(calendarDate.getDate().getAsDate()); 205 | serviceCalendarDates.put(serviceId, calendarDates); 206 | } 207 | 208 | } 209 | 210 | // check for unused stops 211 | 212 | for(Stop stop : gtfsDao.getAllStops()) { 213 | 214 | String stopId = stop.getId().toString(); 215 | 216 | if(!usedStopIds.contains(stopId)) { 217 | result.add(new InvalidValue("stop", "stop_id", stopId, "UnusedStop", "Stop Id " + stopId + " is not used in any trips." , null, Priority.LOW)); 218 | } 219 | } 220 | 221 | 222 | HashMap> blockIntervals = new HashMap>(); 223 | 224 | HashMap duplicateTripHash = new HashMap(); 225 | 226 | String tripKey, blockId; 227 | for(Trip trip : gtfsDao.getAllTrips()) { 228 | 229 | tripId = trip.getId().toString(); 230 | 231 | ArrayList stopTimes = tripStopTimes.get(tripId); 232 | 233 | if(stopTimes == null || stopTimes.isEmpty()) { 234 | InvalidValue iv = new InvalidValue("trip", "trip_id", tripId, "NoStopTimesForTrip", "Trip Id " + tripId + " has no stop times." , null, Priority.HIGH); 235 | iv.route = trip.getRoute(); 236 | result.add(iv); 237 | continue; 238 | } 239 | 240 | Collections.sort(stopTimes, new StopTimeComparator()); 241 | 242 | StopTime previousStopTime = null; 243 | for(StopTime stopTime : stopTimes) { 244 | 245 | if(stopTime.getDepartureTime() < stopTime.getArrivalTime()) { 246 | InvalidValue iv = 247 | new InvalidValue("stop_time", "trip_id", tripId, "StopTimeDepartureBeforeArrival", "Trip Id " + tripId + " stop sequence " + stopTime.getStopSequence() + " departs before arriving.", null, Priority.HIGH); 248 | iv.route = trip.getRoute(); 249 | result.add(iv); 250 | } 251 | 252 | // check for null previous stop time and negative arrival time (int value is -999 if arrival time is empty, e.g. non-timepoint) 253 | if(previousStopTime != null && stopTime.getArrivalTime() > 0) { 254 | 255 | if(stopTime.getArrivalTime() < previousStopTime.getDepartureTime()) { 256 | System.out.println(stopTime.getArrivalTime()); 257 | InvalidValue iv = 258 | new InvalidValue("stop_time", "trip_id", tripId, "StopTimesOutOfSequence", "Trip Id " + tripId + " stop sequence " + stopTime.getStopSequence() + " arrives before departing " + previousStopTime.getStopSequence(), null, Priority.HIGH); 259 | iv.route = trip.getRoute(); 260 | result.add(iv); 261 | 262 | // only capturing first out of sequence stop for now -- could consider collapsing duplicates based on tripId 263 | break; 264 | } 265 | 266 | } 267 | 268 | previousStopTime = stopTime; 269 | } 270 | 271 | 272 | // store trip intervals by block id 273 | 274 | blockId = ""; 275 | 276 | if(trip.getBlockId() != null) 277 | blockId = trip.getBlockId(); 278 | 279 | if(!blockId.isEmpty()) { 280 | 281 | BlockInterval blockInterval = new BlockInterval(); 282 | blockInterval.setTrip(trip); 283 | blockInterval.setStartTime( stopTimes.get(0).getDepartureTime()); 284 | blockInterval.setFirstStop(stopTimes.get(0)); 285 | blockInterval.setLastStop(stopTimes.get(stopTimes.size() -1)); 286 | 287 | if(!blockIntervals.containsKey(blockId)) 288 | blockIntervals.put(blockId, new ArrayList()); 289 | 290 | blockIntervals.get(blockId).add(blockInterval); 291 | 292 | } 293 | 294 | // check for duplicate trips starting at the same time with the same service id 295 | 296 | String stopIds = ""; 297 | 298 | for(StopTime stopTime : stopTimes) { 299 | if (stopTime.getStop() != null && stopTime.getStop().getId() != null) { 300 | stopIds += stopTime.getStop().getId().toString() + ","; 301 | } 302 | } 303 | 304 | tripKey = trip.getServiceId().getId() + "_"+ blockId + "_" + stopTimes.get(0).getDepartureTime() +"_" + stopTimes.get(stopTimes.size() -1).getArrivalTime() + "_" + stopIds; 305 | 306 | if(duplicateTripHash.containsKey(tripKey)) { 307 | String duplicateTripId = duplicateTripHash.get(tripKey); 308 | InvalidValue iv = 309 | new InvalidValue("trip", "trip_id", tripId, "DuplicateTrip", "Trip Ids " + duplicateTripId + " & " + tripId + " are duplicates" , null, Priority.LOW); 310 | iv.route = trip.getRoute(); 311 | result.add(iv); 312 | 313 | } 314 | else 315 | duplicateTripHash.put(tripKey, tripId); 316 | 317 | 318 | } 319 | 320 | // check for overlapping trips within block 321 | 322 | for(Entry> blockIdset : blockIntervals.entrySet()) { 323 | 324 | blockId = blockIdset.getKey(); 325 | ArrayList intervals = blockIntervals.get(blockId); 326 | 327 | Collections.sort(intervals, new BlockIntervalComparator()); 328 | 329 | int iOffset = 0; 330 | for(BlockInterval i1 : intervals) { 331 | for(BlockInterval i2 : intervals.subList(iOffset, intervals.size() - 1)) { 332 | 333 | 334 | String tripId1 = i1.getTrip().getId().toString(); 335 | String tripId2 = i2.getTrip().getId().toString(); 336 | 337 | 338 | if(!tripId1.equals(tripId2)) { 339 | // if trips don't overlap, skip 340 | if(i1.getLastStop().getDepartureTime() <= i2.getFirstStop().getArrivalTime() 341 | || i2.getLastStop().getDepartureTime() <= i1.getFirstStop().getArrivalTime()) 342 | continue; 343 | 344 | // if trips have same service id they overlap 345 | if(i1.getTrip().getServiceId().getId().equals(i2.getTrip().getServiceId().getId())) { 346 | // but if they are already in the result set, ignore 347 | if (!result.containsBoth(tripId1, tripId2, "trip")){ 348 | InvalidValue iv = 349 | new InvalidValue("trip", "block_id", blockId, "OverlappingTripsInBlock", "Trip Ids " + tripId1 + " & " + tripId2 + " overlap and share block Id " + blockId , null, Priority.HIGH); 350 | // not strictly correct; they could be on different routes 351 | iv.route = i1.getTrip().getRoute(); 352 | result.add(iv); 353 | } 354 | } 355 | 356 | else { 357 | // if trips don't share service id check to see if service dates fall on the same days/day of week 358 | 359 | for(Date d1 : serviceCalendarDates.get(i1.getTrip().getServiceId().getId())) { 360 | 361 | if(serviceCalendarDates.get(i2.getTrip().getServiceId().getId()).contains(d1)) { 362 | InvalidValue iv = new InvalidValue("trip", "block_id", blockId, "OverlappingTripsInBlock", "Trip Ids " + tripId1 + " & " + tripId2 + " overlap and share block Id " + blockId , null, Priority.HIGH); 363 | iv.route = i1.getTrip().getRoute(); 364 | result.add(iv); 365 | break; 366 | } 367 | } 368 | } 369 | } 370 | } 371 | } 372 | } 373 | 374 | // check for reversed trip shapes and add to result list 375 | result.append(this.listReversedTripShapes()); 376 | 377 | return result; 378 | 379 | } 380 | 381 | 382 | /** 383 | * Returns a list of coincident DuplicateStops. 384 | * @throws InputOutOfRange if lat/lon of stops can't be transformed to EPSG:4326 385 | * 386 | */ 387 | public ValidationResult duplicateStops() { 388 | // default duplicate stops as coincident with a two meter buffer 389 | return duplicateStops(2.0); 390 | } 391 | 392 | /** 393 | * Returns a list of coincident DuplicateStops. 394 | * 395 | * @param the buffer distance for two stops to be considered duplicate 396 | * 397 | */ 398 | public ValidationResult duplicateStops(Double bufferDistance) { 399 | 400 | ValidationResult result = new ValidationResult(); 401 | 402 | Collection stops = gtfsDao.getAllStops(); 403 | 404 | STRtree stopIndex = new STRtree(); 405 | 406 | HashMap stopProjectedGeomMap = new HashMap(statsService.getStopCount() * 2); 407 | 408 | for(Stop stop : stops) { 409 | 410 | try{ 411 | Geometry geom = GeoUtils.getGeometryFromCoordinate(stop.getLat(), stop.getLon()); 412 | 413 | stopIndex.insert(geom.getEnvelopeInternal(), stop); 414 | 415 | stopProjectedGeomMap.put(stop.getId().toString(), geom); 416 | 417 | } catch (IllegalArgumentException iae) { 418 | result.add(new InvalidValue("stop", "duplicateStops", stop.toString(), "MissingCoordinates", "stop " + stop + " is missing coordinates", null, Priority.MEDIUM)); 419 | } 420 | 421 | } 422 | 423 | stopIndex.build(); 424 | 425 | List duplicateStops = new ArrayList(); 426 | 427 | for(Geometry stopGeom : stopProjectedGeomMap.values()) { 428 | 429 | Geometry bufferedStopGeom = stopGeom.buffer(bufferDistance); 430 | 431 | @SuppressWarnings("unchecked") 432 | List stopCandidates = (List)stopIndex.query(bufferedStopGeom.getEnvelopeInternal()); 433 | 434 | if(stopCandidates.size() > 1) { 435 | 436 | for(Stop stop1 : stopCandidates) { 437 | for(Stop stop2 : stopCandidates) { 438 | 439 | if(stop1.getId() != stop2.getId()) { 440 | 441 | Boolean stopPairAlreadyFound = false; 442 | for(DuplicateStops duplicate : duplicateStops) { 443 | 444 | if((duplicate.stop1.getId().getAgencyId().equals(stop1.getId().getAgencyId()) && duplicate.stop2.getId().getAgencyId().equals(stop2.getId().getAgencyId())) || 445 | (duplicate.stop2.getId().getAgencyId().equals(stop1.getId().getAgencyId()) && duplicate.stop1.getId().getAgencyId().equals(stop2.getId().getAgencyId()))) 446 | stopPairAlreadyFound = true; 447 | } 448 | 449 | if(stopPairAlreadyFound) 450 | continue; 451 | 452 | Geometry stop1Geom = stopProjectedGeomMap.get(stop1.getId().toString()); 453 | Geometry stop2Geom = stopProjectedGeomMap.get(stop2.getId().toString()); 454 | 455 | double distance = stop1Geom.distance(stop2Geom); 456 | 457 | // if stopDistance is within bufferDistance consider duplicate 458 | if(distance <= bufferDistance){ 459 | 460 | // TODO: a good place to check if stops are part of a station grouping 461 | 462 | DuplicateStops duplicateStop = new DuplicateStops(stop1, stop2, distance); 463 | duplicateStops.add(duplicateStop); 464 | result.add(new InvalidValue("stop", "stop_lat,stop_lon", duplicateStop.getStopIds(), "DuplicateStops", duplicateStop.toString(), duplicateStop, Priority.LOW)); 465 | 466 | } 467 | } 468 | 469 | } 470 | } 471 | } 472 | } 473 | 474 | return result; 475 | } 476 | 477 | public ValidationResult listReversedTripShapes() { 478 | return listReversedTripShapes(1.0); 479 | } 480 | /** 481 | * Check for stops that are further away from a shape than expected 482 | * @param minDistance expected max distance from shape to not be included in 483 | * @return 484 | */ 485 | public ValidationResult listStopsAwayFromShape(Double minDistance){ 486 | 487 | List shapeIds = gtfsDao.getAllShapeIds(); 488 | TripPatternCollection tripPatterns = new TripPatternCollection(shapeIds.size() *2); 489 | String problemDescription = "Stop is more than " + minDistance + "m from shape"; 490 | 491 | ValidationResult result = new ValidationResult(); 492 | 493 | Geometry shapeLine, stopGeom; 494 | Stop stop; 495 | Route routeId; 496 | List stopTimes; 497 | List tripsForShape; 498 | 499 | for (AgencyAndId shapeId : shapeIds){ 500 | 501 | shapeLine = GeoUtils.getGeomFromShapePoints( 502 | gtfsDao.getShapePointsForShapeId(shapeId)); 503 | tripsForShape = gtfsDao.getTripsForShapeId(shapeId); 504 | 505 | for (Trip trip: tripsForShape){ 506 | //filter that list by trip patterns, 507 | //where a pattern is a distinct combo of route, shape, and stopTimes 508 | routeId = trip.getRoute(); 509 | stopTimes = gtfsDao.getStopTimesForTrip(trip); 510 | 511 | if (!tripPatterns.addIfNotPresent(routeId, shapeId, stopTimes)){ 512 | 513 | // if any stop is more than minDistance, add to ValidationResult 514 | for (StopTime stopTime : stopTimes){ 515 | stop = stopTime.getStop(); 516 | 517 | try{ 518 | stopGeom = GeoUtils.getGeometryFromCoordinate( 519 | stop.getLat(), stop.getLon()); 520 | if (shapeLine.distance(stopGeom) > minDistance){ 521 | String problem = stop.getId().toString() + " on "+ shapeId.getId(); 522 | InvalidValue iv = new InvalidValue( 523 | "shape", "shape_lat,shape_lon", problem, "StopOffShape", 524 | problemDescription, shapeId.getId(), Priority.MEDIUM); 525 | result.add(iv); 526 | } 527 | } 528 | catch (Exception e){ 529 | result.add(new InvalidValue("stop", "shapeId", shapeId.toString() , "Illegal stopCoord for shape", "", null, Priority.MEDIUM)); 530 | } 531 | } 532 | } 533 | } 534 | 535 | 536 | 537 | // 538 | 539 | } 540 | return result; 541 | } 542 | 543 | public ValidationResult listReversedTripShapes(Double distanceMultiplier) { 544 | 545 | ValidationResult result = new ValidationResult(); 546 | 547 | Collection trips = gtfsDao.getAllTrips(); 548 | 549 | Collection stopTimes = gtfsDao.getAllStopTimes(); 550 | 551 | int numTrips = gtfsDao.getAllTrips().size(); 552 | 553 | HashMap firstStopMap = new HashMap(numTrips *2); 554 | HashMap lastStopMap = new HashMap(numTrips *2); 555 | 556 | // map first and last stops for each trip id 557 | 558 | for(StopTime stopTime : stopTimes) { 559 | String tripId = stopTime.getTrip().getId().toString(); 560 | 561 | if(firstStopMap.containsKey(tripId)) { 562 | if(firstStopMap.get(tripId).getStopSequence() > stopTime.getStopSequence()) 563 | firstStopMap.put(tripId, stopTime); 564 | } 565 | else 566 | firstStopMap.put(tripId, stopTime); 567 | 568 | if(lastStopMap.containsKey(tripId)) { 569 | if(lastStopMap.get(tripId).getStopSequence() < stopTime.getStopSequence()) 570 | lastStopMap.put(tripId, stopTime); 571 | } 572 | else 573 | lastStopMap.put(tripId, stopTime); 574 | } 575 | 576 | Collection shapePoints = gtfsDao.getAllShapePoints(); 577 | 578 | HashMap firstShapePoint = new HashMap(numTrips *2); 579 | HashMap lastShapePoint = new HashMap(numTrips *2); 580 | 581 | // map first and last shape points 582 | 583 | for(ShapePoint shapePoint : shapePoints) { 584 | 585 | String shapeId = shapePoint.getShapeId().getId(); 586 | 587 | if(firstShapePoint.containsKey(shapeId)) { 588 | if(firstShapePoint.get(shapeId).getSequence() > shapePoint.getSequence()) 589 | firstShapePoint.put(shapeId, shapePoint); 590 | } 591 | else 592 | firstShapePoint.put(shapeId, shapePoint); 593 | 594 | if(lastShapePoint.containsKey(shapeId)) { 595 | if(lastShapePoint.get(shapeId).getSequence() < shapePoint.getSequence()) 596 | lastShapePoint.put(shapeId, shapePoint); 597 | } 598 | else 599 | lastShapePoint.put(shapeId, shapePoint); 600 | 601 | } 602 | 603 | String tripId, shapeId; 604 | StopTime firstStop, lastStop; 605 | Coordinate firstStopCoord, lastStopCoord, firstShapeCoord, lastShapeCoord; 606 | Geometry firstShapeGeom, lastShapeGeom, firstStopGeom, lastStopGeom; 607 | 608 | for(Trip trip : trips) { 609 | 610 | tripId = trip.getId().toString(); 611 | if (trip.getShapeId() == null) { 612 | InvalidValue iv = new InvalidValue("trip", "shape_id", tripId, "MissingShape", "Trip " + tripId + " is missing a shape", null, Priority.MEDIUM); 613 | iv.route = trip.getRoute(); 614 | result.add(iv); 615 | continue; 616 | } 617 | shapeId = trip.getShapeId().getId(); 618 | 619 | firstStop = firstStopMap.get(tripId); 620 | lastStop = lastStopMap.get(tripId); 621 | 622 | firstStopCoord = null; 623 | lastStopCoord = null; 624 | firstShapeGeom = null; 625 | lastShapeGeom = null; 626 | firstStopGeom = null; 627 | lastStopGeom = null; 628 | firstShapeCoord = null; 629 | lastShapeCoord = null; 630 | try { 631 | firstStopCoord = new Coordinate(firstStop.getStop().getLat(), firstStop.getStop().getLon()); 632 | lastStopCoord = new Coordinate(lastStop.getStop().getLat(), lastStop.getStop().getLon()); 633 | 634 | firstStopGeom = geometryFactory.createPoint(GeoUtils.convertLatLonToEuclidean(firstStopCoord)); 635 | lastStopGeom = geometryFactory.createPoint(GeoUtils.convertLatLonToEuclidean(lastStopCoord)); 636 | 637 | firstShapeCoord = new Coordinate(firstShapePoint.get(shapeId).getLat(), firstShapePoint.get(shapeId).getLon()); 638 | lastShapeCoord = new Coordinate(lastShapePoint.get(shapeId).getLat(), firstShapePoint.get(shapeId).getLon()); 639 | 640 | firstShapeGeom = geometryFactory.createPoint(GeoUtils.convertLatLonToEuclidean(firstShapeCoord)); 641 | lastShapeGeom = geometryFactory.createPoint(GeoUtils.convertLatLonToEuclidean(lastShapeCoord)); 642 | } catch (Exception any) { 643 | InvalidValue iv = new InvalidValue("trip", "shape_id", tripId, "MissingCoordinates", "Trip " + tripId + " is missing coordinates", null, Priority.MEDIUM); 644 | iv.route = trip.getRoute(); 645 | result.add(iv); 646 | continue; 647 | } 648 | 649 | 650 | firstShapeCoord = new Coordinate(firstShapePoint.get(shapeId).getLat(), firstShapePoint.get(shapeId).getLon()); 651 | lastShapeCoord = new Coordinate(lastShapePoint.get(shapeId).getLat(), firstShapePoint.get(shapeId).getLon()); 652 | 653 | Double distanceFirstStopToStart = firstStopGeom.distance(firstShapeGeom); 654 | Double distanceFirstStopToEnd = firstStopGeom.distance(lastShapeGeom); 655 | 656 | Double distanceLastStopToEnd = lastStopGeom.distance(lastShapeGeom); 657 | Double distanceLastStopToStart = lastStopGeom.distance(firstShapeGeom); 658 | 659 | // check if first stop is x times closer to end of shape than the beginning or last stop is x times closer to start than the end 660 | if(distanceFirstStopToStart > (distanceFirstStopToEnd * distanceMultiplier) && distanceLastStopToEnd > (distanceLastStopToStart * distanceMultiplier)) { 661 | InvalidValue iv = 662 | new InvalidValue("trip", "shape_id", tripId, "ReversedTripShape", "Trip " + tripId + " references reversed shape " + shapeId, null, Priority.MEDIUM); 663 | iv.route = trip.getRoute(); 664 | result.add(iv); 665 | } 666 | } 667 | 668 | return result; 669 | 670 | } 671 | 672 | } 673 | 674 | 675 | --------------------------------------------------------------------------------