├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config.json.example ├── kingco.json.example ├── pom.xml └── src └── main └── java └── com └── conveyal └── geom2gtfs ├── ClusterStopGenerator.java ├── Config.java ├── CsvJoinTable.java ├── ExtendedFeature.java ├── FeatureDoesntDefineTimeWindowException.java ├── GeoMath.java ├── GtfsQueue.java ├── Main.java ├── PicketStopGenerator.java ├── ProtoRoute.java ├── ProtoRouteStop.java ├── ServiceWindow.java ├── ShapefileStopGenerator.java └── StopGenerator.java /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | build 3 | data 4 | .settings 5 | .classpath 6 | .gradle 7 | .project 8 | .DS_Store 9 | *.json 10 | *.zip 11 | .idea/ 12 | geom2gtfs.iml 13 | target/ 14 | 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | # Travis doesn't (yet) support OpenJDK 8 4 | jdk: 5 | - oraclejdk8 6 | 7 | # Replace Travis's default Maven installation step with a no-op. 8 | # This avoids redundantly pre-running 'mvn install -DskipTests' every time. 9 | install: true 10 | 11 | # Replace Travis's default build step. 12 | # Run all Maven phases at once up through verify, install, and deploy. 13 | script: mvn clean deploy 14 | 15 | env: 16 | global: 17 | # encrypted AWS access/secret keys to allow automated deployment to the Conveyal Maven repo on S3 18 | - secure: "b3j0JkkzXuE4YLi4sgCeJdTbZloU0ReWHXAwWNFxkcoBG9Q65KtwZEH21r7fJIT2BGkcqTYXKg8bGxzbmdnJawNFhoWxgI4/NhHErga2n6eqPuPT2TLM8crEawOxFgD6e4FmfYyvRbJ6AgK+bSB4I3a6wNojhxi/aabp/s9hEOU=" 19 | - secure: "fsG2/bAev/6hjzuBZ+uotMMCxfBdwS7L7avnZ3cIeJCiJYxFc2eH8het9RuSdfFD0FwWdtif1g+Cjm93ZU57UiL7d1m2u8lsmLwqRN5IWlKJif/lTBkbHdxDNVv39LuO+1XgApV30qgUfFO8SDxtmROvmQapDvqUp6qv6/THI/I=" 20 | 21 | # If sudo is disabled, CI runs on container based infrastructure (allows caching &c.) 22 | sudo: false 23 | 24 | # Retain the local Maven repository to speed up builds. 25 | cache: 26 | directories: 27 | - "$HOME/.m2/repository" 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Conveyal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | geom2gtfs 2 | ========= 3 | 4 | [geom2gtfs](https://github.com/conveyal/geom2gtfs) is a command line tool that converts each feature in a shapefile into a route in a GTFS file. 5 | 6 | This tool is now deprecated in favor of our graphical [Scenario Editor](https://github.com/conveyal/scenario-editor) tool. 7 | 8 | [![Build Status](https://travis-ci.org/conveyal/geom2gtfs.svg?branch=master)](https://travis-ci.org/conveyal/geom2gtfs) 9 | 10 | Setup 11 | ----- 12 | 13 | To get started, install the [latest JDK](http://www.oracle.com/technetwork/java/javase/downloads/index.html) and [Maven](http://maven.apache.org/). Then: 14 | 15 | ```console 16 | git clone https://github.com/conveyal/geom2gtfs 17 | cd geom2gtfs 18 | mvn package 19 | java -jar target/geom2gtfs.jar 20 | ``` 21 | 22 | Usage 23 | ----- 24 | 25 | Using the geom2gtfs tool is simple, but it requires a carefully prepared shapefile and configuration file. 26 | 27 | The input shapefile must contain only lines, with no multigeometry features. If the features in the shapefile have properties, such as “route” or “mode” or “speed” those properties can be used to modulate the speed of the routes written to the GTFS. It’s possible to use multiple lines to represent the same route if they share a route id property (the name of the property is defined in the config file) and sequential lines run in the same direction and have successive “segment” properties. It’s possible to join a CSV to the shapefile using the geom2gtfs config file, which is how we associated service frequencies with lines. The CSV of service frequencies from King County was compiled using the King County spreadsheet, and looks like this: 28 | 29 | route,peak_am,midday,peak_pm,night,sat,sun 30 | 1,15.0,30.0,15.0,45.0,None,None 31 | 10,10.0,15.0,10.0,30.0,15.0,30.0 32 | 101,15.0,30.0,15.0,30.0,30.0,30.0 33 | 102,30.0,None,25.0,None,None,None 34 | 105,30.0,30.0,30.0,30.0,30.0,60.0 35 | 106,15.0,15.0,15.0,45.0,30.0,30.0 36 | 107,30.0,30.0,30.0,45.0,30.0,30.0 37 | 11,15.0,30.0,15.0,45.0,30.0,30.0 38 | 110,None,None,None,None,None,None 39 | 111,22.0,None,25.0,None,None,None 40 | 113,36.0,None,45.0,None,None,None 41 | 114,60.0,None,60.0,None,None,None 42 | 116EX,22.0,None,25.0,None,None,None 43 | ... 44 | 45 | Finally, it’s possible to have the geom2gtfs tool place stops at a regular spacing along the lines, or to use a shapefile of existing stops. In the case of our King County analysis, I produced a shapefile from the stops.txt of the original GTFS and used that. 46 | 47 | ### An example geom2gtfs configuration file ### 48 | 49 | The config file is a JSON file, which follows with annotations. They aren’t part of the config file. 50 | 51 | { 52 | First, some basic information for the GTFS feed. 53 | 54 | "agency_name":"King County Metro", 55 | "agency_url":"http://metro.kingcounty.gov/", 56 | "agency_timezone":"America/Los_Angeles", 57 | Specify which mode each route will be. 58 | 59 | "gtfs_mode":3, 60 | Set the speed of the GTFS trips, in meters per second. 61 | 62 | "speed":[ 63 | The speed can be a constant, or a list. If it’s a list, each item in the list must have two items. The first is the filter, and the second is the speed. The filter [“ROUTE”,”12”] will match if the property “ROUTE” takes the value “12”. geom2gtfs scans down the list and uses the first match. So if a shapefile feature had property “ROUTE” with value “12” and “express” with value “1”, that feature’s trips would run at 4.0 meters per second. “*” matches everything, which means that 5.4 is the default speed. 64 | 65 | [["ROUTE","12"],4.0], 66 | [["ROUTE","2"],4.0], 67 | [["ROUTE","3"],4.0], 68 | [["express","1"],13.4], 69 | [["ROUTE","193EX"],4.0], 70 | [["express","*"],5.4], 71 | ], 72 | The ‘stops’ section specifies a strategy, which can be “shapefile”, “picket”, or "cluster", and some arguments required by either given strategy. The “shapefile” strategy requires an unprojected point shapefile and a threshold around each linear feature to look for stops in that shapefile. The ‘picket’ strategy takes one named argument ‘spacing’, either a scalar or list of filters like the speed argument. 73 | 74 | "stops":{ 75 | "strategy":"shapefile", 76 | "filename":"data/kingco/kingco_stops.shp", 77 | "threshold":0.0002, 78 | }, 79 | 80 | Alternately, one can use the cluster strategy. This strategy takes a spacing argument like the picket strategy, but unlike the picket strategy it will use the same stops for multiple routes that travel along the same roads. It also takes a `threshold` property (default 100m), which is the maximum distance (in meters) stops will be moved from their ideal locations in order to coincide with an existing stop. 81 | 82 | You can also specify a property `osmfiles`, which is a list of OSM PBF files whose roads and intersections will have stops snapped to them. 83 | 84 | You can specify a property `create_stops`. If true (default), stops will be created even if there is nothing nearby to snap them to. If false, these stop locations will be skipped (useful for routes that run along highways, for example). 85 | 86 | Specify the name of the shapefile property where the route id is kept. If this property is omitted, or if the shapefile doesn't contain it, route ID's will be generated. This of course means that each route can be represented by but a single feature. 87 | 88 | "route_id_prop_name":"ROUTE", 89 | Specify the name of the shapefile property where the route name is kept. Will be set to the route ID if this property is omitted or the shapefile doesn't contain it. 90 | 91 | "route_name_prop_name":"ROUTE", 92 | Specify the ‘service windows’. Each entry in the list specifies the service window name, is starting time, and its ending time, both in hours since midnight. For example “peak_am” runs from 6am to 9am. The shapefile must contain a property with the same name as each service window, filled out with the service level for that frequency. For example the route “1” has a property “peak_am” with value “15.0”. 93 | 94 | "service_windows":[ 95 | ["peak_am",6,9], 96 | ["midday",9,15], 97 | ["peak_pm",15,18], 98 | ["night",18,24], 99 | ], 100 | Optionally, join a CSV to the shapefile features. In the case of our shapefile, none of the service windows are actually properties of the shapefile features; they are joined in from this shapefile. 101 | 102 | "csv_join":{ 103 | "filename":"data/kingco/prop_freqs.csv", 104 | "csv_col":"route", 105 | "shp_col":"ROUTE", 106 | }, 107 | Optionally, set a filter that must pass for a feature to be converted to a GTFS route. 108 | 109 | "filters":[ 110 | ["CATEGORY","topo"], 111 | ], 112 | Set the start and end date of the service calendar. Our hypothetical GTFS will be valid from the start of 2014 to the start of 2015. 113 | 114 | "start_date":"20140101", 115 | "end_date":"20150101", 116 | Specify whether the service level values are periods (headways), the amount of time between departures; or frequencies, the number of departures in an hour. 117 | 118 | "use_periods":true, 119 | 120 | If you have not specified service level values for any of the features in your file, you can set the default service level. This is controlled by use_periods, so may be a frequency or a headway (in minutes) depending on the value of that setting. 121 | 122 | "service_level": 15, 123 | 124 | Set whether or not service should run in both directions of a shapefile line. If “is_bidirectional” is false, you’ll need to make a shapefile feature for each route direction. 125 | 126 | "is_bidirectional":true, 127 | Set whether the GTFS should contain precise times, or whether it should be frequency-based. 128 | 129 | "exact":true 130 | } 131 | 132 | This config file, combined with a shapefile that I manually drew for every one of the fifty revised routes in King County, produced a GTFS representing a reasonable approximation of the realigned routes. That’s only a part of the puzzle though. For the next part, we’ll need resample_gtfs. 133 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "agency_name":"Buenos Aires Transit", 3 | "agency_url":"http://www.buenosaires.gob.ar/subte", 4 | "agency_timezone":"America/Argentina/Buenos_Aires", 5 | "gtfs_mode":[ 6 | [["MODE_ID","3"],0], 7 | [["MODE_ID","1"],1], 8 | [["MODE_ID","2"],2], 9 | [["MODE_ID","*"],3] 10 | ], 11 | "speed":[ 12 | [["MODE_ID","3"],5.4], 13 | [["MODE_ID","1"],8.9], 14 | [["MODE_ID","2"],13.4], 15 | [["MODE_ID","*"],5.4] 16 | ], 17 | "spacing":[ 18 | [["MODE_ID","3"],500], 19 | [["MODE_ID","1"],700], 20 | [["MODE_ID","2"],1500], 21 | [["MODE_ID","*"],400] 22 | ], 23 | "route_id_prop_name":"ROUTE_ID", 24 | "route_name_prop_name":"ROUTE_NAME", 25 | "service_windows":[ 26 | ["FRECHPM",6,9], 27 | ["FRECEPM",9,11], 28 | ["FRECALM",11,13], 29 | ["FRECEPT",13,15], 30 | ["FRECHPT",15,18] 31 | ], 32 | "start_date":"20140101", 33 | "end_date":"20150101" 34 | } -------------------------------------------------------------------------------- /kingco.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "agency_name":"King County Metro", 3 | "agency_url":"http://metro.kingcounty.gov/", 4 | "agency_timezone":"America/Los_Angeles", 5 | "gtfs_mode":3, 6 | "speed":[ 7 | [["ROUTE","12"],4.0], 8 | [["ROUTE","2"],4.0], 9 | [["ROUTE","3"],4.0], 10 | [["express","1"],13.4], 11 | [["ROUTE","193EX"],4.0], 12 | [["express","*"],5.4], 13 | ], 14 | "stops":{ 15 | "strategy":"shapefile", 16 | "filename":"data/kingco/kingco_stops.shp", 17 | "threshold":0.0002, 18 | }, 19 | "route_id_prop_name":"ROUTE", 20 | "route_name_prop_name":"ROUTE", 21 | "service_windows":[ 22 | ["peak_am",6,9], 23 | ["midday",9,15], 24 | ["peak_pm",15,18], 25 | ["night",18,24], 26 | ], 27 | "csv_join":{ 28 | "filename":"data/kingco/prop_freqs.csv", 29 | "csv_col":"route", 30 | "shp_col":"ROUTE", 31 | }, 32 | "filters":[ 33 | ["CATEGORY","topo"], 34 | ], 35 | "start_date":"20140101", 36 | "end_date":"20150101", 37 | "use_periods":true, 38 | "wait_factor":2.5, 39 | "is_bidirectional":true, 40 | "exact":true 41 | } 42 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.conveyal 6 | geom2gtfs 7 | 0.1-SNAPSHOT 8 | jar 9 | 10 | geom2gtfs 11 | http://maven.apache.org 12 | 13 | 14 | 15 | scm:git:https://github.com/conveyal/geom2gtfs.git 16 | scm:git:ssh://git@github.com/conveyal/geom2gtfs.git 17 | https://github.com/conveyal/geom2gtgs.git 18 | 19 | 20 | 21 | 22 | 23 | 24 | conveyal-maven-repo 25 | Conveyal Maven Repository 26 | s3://maven.conveyal.com/ 27 | 28 | 29 | 30 | 31 | UTF-8 32 | 33 | 34 | 35 | 36 | 37 | org.apache.maven.plugins 38 | maven-compiler-plugin 39 | 3.2 40 | 41 | 1.8 42 | 1.8 43 | 44 | 45 | 46 | 47 | org.apache.maven.plugins 48 | maven-shade-plugin 49 | 2.2 50 | 51 | 52 | 53 | package 54 | shade 55 | 56 | geom2gtfs 57 | 58 | 59 | 60 | com.conveyal.geom2gtfs.Main 61 | 62 | 63 | 64 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | org.kuali.maven.wagons 79 | maven-s3-wagon 80 | 1.2.1 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | osgeo 90 | Open Source Geospatial Foundation Repository 91 | http://download.osgeo.org/webdav/geotools/ 92 | 93 | 94 | conveyal 95 | Conveyal Maven Repository 96 | http://maven.conveyal.com/ 97 | 98 | 99 | 100 | 101 | 102 | org.geotools 103 | gt-shapefile 104 | 10.5 105 | 106 | 107 | org.onebusaway 108 | onebusaway-gtfs 109 | 1.3.3 110 | 111 | 112 | com.google.guava 113 | guava 114 | 18.0 115 | 116 | 117 | org.json 118 | json 119 | 20141113 120 | 121 | 122 | com.conveyal 123 | osm-lib 124 | 0.1-SNAPSHOT 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/main/java/com/conveyal/geom2gtfs/ClusterStopGenerator.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.geom2gtfs; 2 | 3 | import com.conveyal.osmlib.Node; 4 | import com.conveyal.osmlib.OSM; 5 | import com.conveyal.osmlib.Way; 6 | import com.vividsolutions.jts.geom.*; 7 | import com.vividsolutions.jts.index.SpatialIndex; 8 | import com.vividsolutions.jts.index.quadtree.Quadtree; 9 | import com.vividsolutions.jts.linearref.LinearLocation; 10 | import com.vividsolutions.jts.linearref.LocationIndexedLine; 11 | import org.json.JSONArray; 12 | import org.json.JSONObject; 13 | import org.onebusaway.csv_entities.EntityHandler; 14 | import org.onebusaway.csv_entities.exceptions.CsvEntityIOException; 15 | import org.onebusaway.gtfs.model.AgencyAndId; 16 | import org.onebusaway.gtfs.model.Stop; 17 | import org.onebusaway.gtfs.serialization.GtfsReader; 18 | import org.opengis.feature.GeometryAttribute; 19 | 20 | import java.io.File; 21 | import java.io.IOException; 22 | import java.util.List; 23 | 24 | /** 25 | * A stop generator that creates stops at roughly the requested spacing 26 | * (within the requested tolerance), and shares stops between generated routes. 27 | * 28 | * @author mattwigway 29 | */ 30 | public class ClusterStopGenerator implements StopGenerator { 31 | // this is final because the subclass stoploader accesses it. 32 | public final SpatialIndex stopIndex; 33 | public SpatialIndex wayIndex; 34 | public SpatialIndex createdStopIndex; 35 | 36 | private JSONObject data; 37 | 38 | private static GeometryFactory geometryFactory = 39 | new GeometryFactory(new PrecisionModel(PrecisionModel.FIXED), 4326); 40 | /** 41 | * How far to each side of the route we look for candidate stops, in meters. 42 | */ 43 | public double threshold; 44 | 45 | /** 46 | * Should we create stops if we can't find anything to snap to? 47 | */ 48 | public boolean createUnmatchedStops; 49 | 50 | public ClusterStopGenerator(JSONObject data) { 51 | this.data = data; 52 | 53 | stopIndex = new Quadtree(); 54 | wayIndex = new Quadtree(); 55 | createdStopIndex = new Quadtree(); 56 | threshold = data.has("threshold") ? data.getDouble("threshold") : 100D; 57 | createUnmatchedStops = data.has("create_stops") ? data.getBoolean("create_stops") : true; 58 | 59 | // Load OSM files 60 | if (data.has("osmfiles")) { 61 | JSONArray files = data.getJSONArray("osmfiles"); 62 | for (int i = 0; i < files.length(); i++) { 63 | String fn = files.getString(i); 64 | System.err.println("Processing OSM file " + fn); 65 | 66 | // store OSM in temporary file 67 | OSM osm = new OSM(null); 68 | osm.intersectionDetection = true; 69 | osm.readFromFile(fn); 70 | 71 | for (Way way : osm.ways.values()) { 72 | // todo: pedestrian = no 73 | if (way.hasTag("highway") && !"motorway".equals(way.getTag("highway")) 74 | && way.nodes.length >= 2) { 75 | 76 | // create wayinfo 77 | WayInfo wi = new WayInfo(); 78 | wi.coords = new Coordinate[way.nodes.length]; 79 | wi.intersections = new boolean[way.nodes.length]; 80 | 81 | for (int coordIdx = 0; coordIdx < way.nodes.length; coordIdx++) { 82 | Node node = osm.nodes.get(way.nodes[coordIdx]); 83 | wi.coords[coordIdx] = new Coordinate(node.getLon(), node.getLat()); 84 | wi.intersections[coordIdx] = osm.intersectionNodes.contains(way.nodes[coordIdx]); 85 | } 86 | 87 | LineString ls = geometryFactory.createLineString(wi.coords); 88 | wayIndex.insert(ls.getEnvelopeInternal(), wi); 89 | } 90 | } 91 | } 92 | } 93 | 94 | // Load GTFS files 95 | if (data.has("gtfsfiles")) { 96 | 97 | JSONArray files = data.getJSONArray("gtfsfiles"); 98 | for (int fileIdx = 0; fileIdx < files.length(); fileIdx++) { 99 | String fileName = files.getString(fileIdx); 100 | System.err.println("Processing GTFS file " + fileName); 101 | 102 | try { 103 | GtfsReader reader = new GtfsReader(); 104 | reader.setInputLocation(new File(fileName)); 105 | reader.addEntityHandler(new StopLoader(fileIdx)); 106 | reader.run(); 107 | } catch (CsvEntityIOException e) { 108 | Throwable cause = e.getCause(); 109 | if (cause instanceof AllStopsLoadedException) { 110 | // this is expected; all the stops have been loaded. 111 | System.err.println("Loaded " + ((AllStopsLoadedException) cause).count + " stops" ); 112 | } else { 113 | // rethrow 114 | throw e; 115 | } 116 | } catch (IOException e) { 117 | System.err.println("Unable to load GTFS file " + fileName); 118 | } 119 | } 120 | } 121 | } 122 | 123 | @Override 124 | public ProtoRoute makeProtoRoute(ExtendedFeature exft, Double speed) throws Exception { 125 | ProtoRoute out = new ProtoRoute(); 126 | out.speed = speed; 127 | 128 | // find candidate stops 129 | GeometryAttribute geomAttr = exft.feat.getDefaultGeometryProperty(); 130 | MultiLineString geom = (MultiLineString) geomAttr.getValue(); 131 | 132 | LineString ls = (LineString) geom.getGeometryN(0); 133 | LocationIndexedLineInLocalCoordinateSystem indexed = new LocationIndexedLineInLocalCoordinateSystem(ls.getCoordinates()); 134 | 135 | // figure out the offsets to each coordinate in the line 136 | Coordinate[] coords = ls.getCoordinates(); 137 | double[] metersAlongLine = new double[coords.length]; 138 | 139 | metersAlongLine[0] = 0; // by construction, the first coordinate is 0 meters along the line 140 | 141 | for (int i = 1; i < coords.length; i++) { 142 | metersAlongLine[i] = metersAlongLine[i - 1] + GeoMath.greatCircle(coords[i - 1], coords[i]); 143 | } 144 | 145 | out.length = metersAlongLine[metersAlongLine.length - 1]; 146 | 147 | double spacing = Config.getSpacing(exft, this.data); 148 | 149 | // find stops near each "ideal" location on the line 150 | for (double offset = 0; offset < metersAlongLine[metersAlongLine.length - 1]; offset += spacing) { 151 | // find the point for which we want to find a protoroutestop 152 | int right = 1; 153 | while (metersAlongLine[right] < offset && right < metersAlongLine.length) 154 | right++; 155 | 156 | double length = (metersAlongLine[right] - metersAlongLine[right - 1]); 157 | 158 | Coordinate ideal; 159 | if (length > 0.00000000001) { 160 | double frac = (offset - metersAlongLine[right - 1]) / length; 161 | ideal = GeoMath.interpolate(coords[right - 1], coords[right], frac); 162 | } 163 | else { 164 | // We have landed with our ideal stop exactly on top of two duplicated coordinates. 165 | // While this seems improbable, it can happen if the coordinates at the start are duplicated 166 | ideal = coords[right]; 167 | } 168 | 169 | ProtoRouteStop prs = getProtoRouteStopForCoord(ideal, indexed); 170 | 171 | if (prs != null) { 172 | // don't add the same stop twice in a row. 173 | if (!out.ret.isEmpty() && prs.stop.equals(out.ret.get(out.ret.size() - 1).stop)) 174 | continue; 175 | 176 | // set the distance appropriately 177 | // away from this one, not from some ideal location. 178 | // Note that we do not set the distance to this stop, but rather the distance to the ideal location 179 | // of this stop. this avoids issues with loop routes, where the same stop might have multiple distances 180 | // distance just needs to be monotonically increasing, no need to be super-accurate in a ratio sense. 181 | prs.dist = offset; 182 | out.add(prs); 183 | } 184 | } 185 | 186 | return out; 187 | } 188 | 189 | /** 190 | * Find the best stop near the given coordinate. 191 | */ 192 | private ProtoRouteStop getProtoRouteStopForCoord(Coordinate ideal, LocationIndexedLineInLocalCoordinateSystem routeGeometry) { 193 | // first look for existing nearby stops 194 | Envelope env = new Envelope(ideal); 195 | 196 | // get the upper bound 197 | double thresholdDegrees = GeoMath.upperBoundDegreesForThreshold(ideal.y, threshold); 198 | env.expandBy(thresholdDegrees); 199 | 200 | @SuppressWarnings("unchecked") 201 | List stops = stopIndex.query(env); 202 | 203 | if (!stops.isEmpty()) { 204 | // hooray we found existing stops! 205 | double bestDistance = Double.MAX_VALUE; 206 | Stop best = null; 207 | 208 | for (Stop stop : stops) { 209 | Coordinate stopCoord = new Coordinate(stop.getLon(), stop.getLat()); 210 | double dist = GeoMath.greatCircle(stopCoord, ideal); 211 | 212 | // make sure it's not on a completely different street 213 | Coordinate pointOnRoute = routeGeometry.extractPoint(routeGeometry.project(stopCoord)); 214 | double distFromRoute = GeoMath.greatCircle(stopCoord, pointOnRoute); 215 | 216 | // TODO arbitrary hardcoded cutoff for distance from route 217 | if (dist < bestDistance && dist <= threshold && distFromRoute < 50) { 218 | bestDistance = dist; 219 | best = stop; 220 | } 221 | } 222 | 223 | if (best != null) { 224 | return new ProtoRouteStop(best, 0); 225 | } 226 | } 227 | 228 | 229 | // Look for nearby ways 230 | 231 | 232 | @SuppressWarnings("unchecked") 233 | List ways = wayIndex.query(env); 234 | 235 | Coordinate bestPoint = null; 236 | 237 | if (!ways.isEmpty()) { 238 | // OK, snap to nearest way 239 | // note that the spatial index only contains walkable ways 240 | 241 | Coordinate point; 242 | int left, right; 243 | LinearLocation loc; 244 | double bestDistance = Double.MAX_VALUE; 245 | double dist, leftDist, rightDist; 246 | 247 | for (WayInfo wayInfo : ways) { 248 | LocationIndexedLineInLocalCoordinateSystem way = 249 | new LocationIndexedLineInLocalCoordinateSystem(wayInfo.coords); 250 | loc = way.project(ideal); 251 | point = way.extractPoint(loc); 252 | dist = GeoMath.greatCircle(point, ideal); 253 | 254 | if (dist < bestDistance && dist <= threshold) { 255 | bestDistance = dist; 256 | bestPoint = point; 257 | 258 | // we don't blithely add one to the right segment index, because this isn't 259 | // actually a segment index; if the point is past the end of the line the 260 | // segment index is the index of the last coordinate 261 | // AFAIK segment index cannot be negative. 262 | left = right = loc.getSegmentIndex(); 263 | 264 | // but check and give a useful error message in case my assumption is incorrect. 265 | if (left < 0) { 266 | throw new RuntimeException("Got negative segment index."); 267 | } 268 | 269 | // find the next and previous intersections, if they exist 270 | while (!wayInfo.intersections[left] && left > 0) left--; 271 | while (!wayInfo.intersections[right] && right < wayInfo.coords.length - 1) right++; 272 | 273 | leftDist = GeoMath.greatCircle(wayInfo.coords[left], ideal); 274 | rightDist = GeoMath.greatCircle(wayInfo.coords[right], ideal); 275 | 276 | if ((left == right || leftDist <= rightDist) && leftDist <= threshold) { 277 | // we don't reset bestDistance but instead leave it as the distance to the 278 | // nearest point on the way. So we're saying "snap to an intersection, if possible, 279 | // on the closest way" 280 | bestPoint = wayInfo.coords[left]; 281 | } 282 | 283 | else if (rightDist <= leftDist && rightDist <= threshold) { 284 | bestPoint = wayInfo.coords[right]; 285 | } 286 | 287 | } 288 | } 289 | } 290 | 291 | // if we didn't find anything to snap to, deal with it 292 | if (bestPoint == null) { 293 | if (createUnmatchedStops) { 294 | bestPoint = ideal; 295 | } 296 | else { 297 | System.err.println("Could not find stop location near " + ideal.y + ", " + ideal.x); 298 | return null; 299 | } 300 | } 301 | 302 | ProtoRouteStop prs = new ProtoRouteStop(bestPoint, 0); 303 | 304 | // add the newly-created stop to the index 305 | stopIndex.insert(new Envelope(prs.coord), prs.stop); 306 | 307 | return prs; 308 | } 309 | 310 | /** 311 | * Holds just enough information about a way to be able to snap to its nodes. 312 | * @author mattwigway 313 | * 314 | */ 315 | private static class WayInfo { 316 | public Coordinate[] coords; 317 | 318 | /** intersections[i] is true if node i is an intersection, false otherwise */ 319 | public boolean[] intersections; 320 | } 321 | 322 | /** 323 | * Load just the stops from a GTFS feed. 324 | * 325 | * Once all stops have been loaded, raises an AllStopsLoadedException to prevent loading the 326 | * rest of the feed. 327 | */ 328 | private class StopLoader implements EntityHandler { 329 | private int gtfsFile; 330 | private int count; 331 | 332 | /** 333 | * Construct a new StopLoader 334 | * @param gtfsFile Stop IDs will be prefixed with this to avoid namespace collisions. 335 | */ 336 | public StopLoader (int gtfsFile) { 337 | count = 0; 338 | this.gtfsFile = gtfsFile; 339 | } 340 | 341 | @Override 342 | public void handleEntity(Object o) { 343 | if (o instanceof Stop) { 344 | Stop stop = (Stop) o; 345 | 346 | // don't snap to stations 347 | if (stop.getLocationType() == 1) 348 | return; 349 | 350 | count++; 351 | 352 | // referential integrity 353 | stop.setParentStation(null); 354 | 355 | Coordinate coord = new Coordinate(stop.getLon(), stop.getLat()); 356 | 357 | AgencyAndId existingId = stop.getId(); 358 | AgencyAndId newId = 359 | new AgencyAndId(Main.DEFAULT_AGENCY_ID, 360 | "" + gtfsFile + "_" + existingId.getId() 361 | ); 362 | 363 | stop.setId(newId); 364 | 365 | stopIndex.insert(new Envelope(coord), stop); 366 | } 367 | else if (count > 0) { 368 | // OBA processes the files sequentially, so if we have seen a stop in the past 369 | // but this is not a stop, we are done 370 | 371 | throw new AllStopsLoadedException(count); 372 | } 373 | } 374 | } 375 | 376 | /** 377 | * Indicates that all the stops have been loaded. 378 | * 379 | * This is unchecked because we can't throw an exception inside HandleEntity. 380 | */ 381 | private static class AllStopsLoadedException extends RuntimeException { 382 | public int count; 383 | 384 | public AllStopsLoadedException (int count) { 385 | this.count = count; 386 | } 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/main/java/com/conveyal/geom2gtfs/Config.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.geom2gtfs; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.net.MalformedURLException; 6 | import java.nio.file.Files; 7 | import java.util.ArrayList; 8 | import java.util.Calendar; 9 | import java.util.Date; 10 | import java.util.List; 11 | 12 | import org.json.JSONArray; 13 | import org.json.JSONException; 14 | import org.json.JSONObject; 15 | import org.opengis.feature.Feature; 16 | 17 | public class Config { 18 | 19 | JSONObject data; 20 | private boolean DEFAULT_USE_PERIODS = false; 21 | private double DEFAULT_WAIT_FACTOR = 1.0; 22 | private boolean DEFAULT_EXACT = false; 23 | private static final boolean DEFAULT_TOLERANT = true; 24 | 25 | public Config(String config_fn) throws IOException { 26 | File ff = new File(config_fn); 27 | String jsonStr = new String( Files.readAllBytes( ff.toPath() ) ); 28 | data = new JSONObject( jsonStr ); 29 | } 30 | 31 | public String getAgencyName() { 32 | try{ 33 | return data.getString("agency_name"); 34 | }catch(JSONException e){ 35 | return null; 36 | } 37 | } 38 | 39 | public String getAgencyUrl() { 40 | try{ 41 | return data.getString("agency_url"); 42 | }catch(JSONException e){ 43 | return null; 44 | } 45 | } 46 | 47 | public String getAgencyTimezone() { 48 | try{ 49 | return data.getString("agency_timezone"); 50 | }catch(JSONException e){ 51 | return null; 52 | } 53 | } 54 | 55 | public Integer getMode(ExtendedFeature feat) { 56 | Object modeObj = data.get("gtfs_mode"); 57 | if( Integer.class.isInstance( modeObj) ){ 58 | return (Integer)modeObj; 59 | } 60 | 61 | // else it should be an array; 62 | JSONArray gtfsModeFilters = (JSONArray)modeObj; 63 | 64 | for(int i=0; i getServiceWindows() { 224 | List ret = new ArrayList(); 225 | 226 | JSONArray windows = data.getJSONArray("service_windows"); 227 | for(int i=0; i records; 21 | private ArrayList header; 22 | 23 | public CsvJoinTable(String filename, String csvCol, String shpCol) throws IOException { 24 | this.csvCol = csvCol; 25 | this.shpCol = shpCol; 26 | this.records = new HashMap(); 27 | 28 | BufferedReader br = new BufferedReader(new FileReader(new File(filename))); 29 | String headerStr = br.readLine(); 30 | header = new ArrayList( Arrays.asList( headerStr.split(",") ) ); 31 | 32 | int keyCol = header.indexOf(this.csvCol); 33 | 34 | String line; 35 | while ((line = br.readLine()) != null) { 36 | String[] row = line.split(","); 37 | String key = row[keyCol]; 38 | records.put(key, row); 39 | } 40 | br.close(); 41 | } 42 | 43 | public Map getExtraFields(Feature feat) { 44 | Map ret = new HashMap(); 45 | 46 | String key = feat.getProperty(shpCol).getValue().toString(); 47 | String[] fields = records.get(key); 48 | if( fields==null ){ 49 | return ret; 50 | } 51 | 52 | for(int i=0; i extraFields; 17 | Feature feat; 18 | 19 | public ExtendedFeature(Feature feat, CsvJoinTable csvJoin) { 20 | this.feat = feat; 21 | if (csvJoin != null) { 22 | this.extraFields = csvJoin.getExtraFields(feat); 23 | } else { 24 | this.extraFields = null; 25 | } 26 | } 27 | 28 | /** get a raw property */ 29 | public Object getPropertyRaw (String key) { 30 | if (extraFields != null) { 31 | String ret = extraFields.get(key); 32 | if (ret != null) { 33 | return ret; 34 | } 35 | } 36 | 37 | Property prop = feat.getProperty(key); 38 | if (prop == null) { 39 | return null; 40 | } 41 | Object val = prop.getValue(); 42 | return val; 43 | } 44 | 45 | public String getProperty(String key) { 46 | Object val = getPropertyRaw(key); 47 | return val != null ? val.toString() : null; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/conveyal/geom2gtfs/FeatureDoesntDefineTimeWindowException.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.geom2gtfs; 2 | 3 | public class FeatureDoesntDefineTimeWindowException extends Exception { 4 | 5 | private static final long serialVersionUID = -5855030157442523986L; 6 | String propName; 7 | 8 | public FeatureDoesntDefineTimeWindowException(String propName) { 9 | this.propName = propName; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/conveyal/geom2gtfs/GeoMath.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.geom2gtfs; 2 | 3 | import com.vividsolutions.jts.geom.Coordinate; 4 | 5 | public class GeoMath { 6 | static double greatCircle(double x1deg, double y1deg, double x2deg, double y2deg){ 7 | double x1 = Math.toRadians(x1deg); 8 | double y1 = Math.toRadians(y1deg); 9 | double x2 = Math.toRadians(x2deg); 10 | double y2 = Math.toRadians(y2deg); 11 | 12 | // haversine formula 13 | double a = Math.pow(Math.sin((x2-x1)/2), 2) 14 | + Math.cos(x1) * Math.cos(x2) * Math.pow(Math.sin((y2-y1)/2), 2); 15 | 16 | // great circle distance in radians 17 | double angle2 = 2 * Math.asin(Math.min(1, Math.sqrt(a))); 18 | 19 | // convert back to degrees 20 | angle2 = Math.toDegrees(angle2); 21 | 22 | // each degree on a great circle of Earth is 60 nautical miles 23 | double distNautMiles = 60 * angle2; 24 | 25 | double distMeters = distNautMiles * 1852; 26 | return distMeters; 27 | } 28 | 29 | static double greatCircle( Coordinate p1, Coordinate p2 ){ 30 | return greatCircle( p1.x, p1.y, p2.x, p2.y ); 31 | } 32 | 33 | static Coordinate interpolate(Coordinate p1, Coordinate p2, 34 | double index) { 35 | 36 | double x = (p2.x-p1.x)*index + p1.x; 37 | double y = (p2.y-p1.y)*index + p1.y; 38 | 39 | return new Coordinate(x,y); 40 | } 41 | 42 | /** 43 | * The maximum number of degrees that is needed to represent the given threshold (in meters) at the given latitude. 44 | */ 45 | static double upperBoundDegreesForThreshold(double latitude, double threshold) { 46 | // get the number of meters in a degree of longitude at this latitude 47 | double metersPerDegree = Math.PI * Math.cos(Math.toRadians(latitude)) * 6371000 / 180; 48 | return threshold / metersPerDegree; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/conveyal/geom2gtfs/GtfsQueue.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.geom2gtfs; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.onebusaway.gtfs.model.Agency; 7 | import org.onebusaway.gtfs.model.Frequency; 8 | import org.onebusaway.gtfs.model.Route; 9 | import org.onebusaway.gtfs.model.ServiceCalendar; 10 | import org.onebusaway.gtfs.model.Stop; 11 | import org.onebusaway.gtfs.model.StopTime; 12 | import org.onebusaway.gtfs.model.Trip; 13 | import org.onebusaway.gtfs.serialization.GtfsWriter; 14 | 15 | public class GtfsQueue { 16 | public List agencies = new ArrayList(); 17 | public List routes = new ArrayList(); 18 | public List stops = new ArrayList(); 19 | public List trips = new ArrayList(); 20 | public List frequencies = new ArrayList(); 21 | public List stoptimes = new ArrayList(); 22 | public List calendars = new ArrayList(); 23 | 24 | public void dumpToWriter(GtfsWriter gtfsWriter) { 25 | for(Agency agency : agencies){ 26 | gtfsWriter.handleEntity( agency ); 27 | } 28 | for(Route route : routes){ 29 | gtfsWriter.handleEntity(route); 30 | } 31 | for(Trip trip : trips){ 32 | gtfsWriter.handleEntity(trip); 33 | } 34 | for(Stop stop : stops){ 35 | gtfsWriter.handleEntity(stop); 36 | } 37 | for(StopTime stoptime : stoptimes){ 38 | gtfsWriter.handleEntity(stoptime); 39 | } 40 | for(Frequency fr : frequencies){ 41 | gtfsWriter.handleEntity(fr); 42 | } 43 | for(ServiceCalendar sc : calendars){ 44 | gtfsWriter.handleEntity(sc); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/conveyal/geom2gtfs/Main.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.geom2gtfs; 2 | 3 | import com.vividsolutions.jts.geom.Geometry; 4 | import org.geotools.data.DataStore; 5 | import org.geotools.data.DataStoreFinder; 6 | import org.geotools.data.simple.SimpleFeatureCollection; 7 | import org.geotools.data.simple.SimpleFeatureIterator; 8 | import org.geotools.data.simple.SimpleFeatureSource; 9 | import org.geotools.geometry.jts.JTS; 10 | import org.geotools.referencing.CRS; 11 | import org.geotools.referencing.crs.DefaultGeographicCRS; 12 | import org.onebusaway.gtfs.model.*; 13 | import org.onebusaway.gtfs.model.calendar.ServiceDate; 14 | import org.onebusaway.gtfs.serialization.GtfsWriter; 15 | import org.opengis.feature.Feature; 16 | import org.opengis.feature.simple.SimpleFeature; 17 | import org.opengis.feature.simple.SimpleFeatureType; 18 | import org.opengis.referencing.crs.CoordinateReferenceSystem; 19 | import org.opengis.referencing.operation.MathTransform; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.net.MalformedURLException; 24 | import java.net.URL; 25 | import java.util.*; 26 | import java.util.Map.Entry; 27 | 28 | 29 | public class Main { 30 | 31 | static final String DEFAULT_AGENCY_ID = "0"; 32 | private static final String DEFAULT_CAL_ID = "0"; 33 | 34 | private Config config; 35 | 36 | // used to generate ids and names for routes that do not have them 37 | private int nextId = 0; 38 | 39 | private GtfsQueue queue = null; 40 | 41 | /** 42 | * wrapper main function creates instance so that calling geom2gtfs twice in the same JVM doesn't cause conflicts 43 | * (this can happen in analyst-server, see conveyal/analyst-server#262) 44 | */ 45 | public static void main (String[] args) throws Exception { 46 | if (args.length < 3) { 47 | System.out.println("usage: cmd shapefile_fn config_fn output_fn"); 48 | return; 49 | } 50 | new Main().main(args[0], args[1], args[2]); 51 | } 52 | 53 | public void main(String fn, String config_fn, String output_fn) throws Exception { 54 | config = new Config(config_fn); 55 | 56 | // check if config specifies a csv join 57 | CsvJoinTable csvJoin = config.getCsvJoin(); 58 | 59 | queue = new GtfsQueue(); 60 | 61 | ServiceCalendar cal = new ServiceCalendar(); 62 | cal.setMonday(1); 63 | cal.setTuesday(1); 64 | cal.setWednesday(1); 65 | cal.setThursday(1); 66 | cal.setFriday(1); 67 | cal.setSaturday(1); 68 | cal.setSunday(1); 69 | cal.setStartDate(new ServiceDate(config.getStartDate())); 70 | cal.setEndDate(new ServiceDate(config.getEndDate())); 71 | cal.setServiceId(new AgencyAndId(DEFAULT_AGENCY_ID, DEFAULT_CAL_ID)); 72 | queue.calendars.add(cal); 73 | 74 | Agency agency = new Agency(); 75 | agency.setId(DEFAULT_AGENCY_ID); 76 | agency.setName(config.getAgencyName()); 77 | agency.setUrl(config.getAgencyUrl()); 78 | agency.setTimezone(config.getAgencyTimezone()); 79 | queue.agencies.add(agency); 80 | 81 | List features = getFeatures(fn); 82 | 83 | List extFeatures = joinFeatures( features, csvJoin ); 84 | 85 | extFeatures = filterFeatures( extFeatures ); 86 | 87 | Map> featureGroups = groupFeatures( extFeatures ); 88 | 89 | StopGenerator stopGenerator = config.getStopGenerator(); 90 | 91 | for( Entry> group : featureGroups.entrySet() ){ 92 | featToGtfs(group.getValue(), agency, stopGenerator, group.getKey()); 93 | } 94 | 95 | System.out.println( "writing to "+output_fn ); 96 | GtfsWriter gtfsWriter = new GtfsWriter(); 97 | gtfsWriter.setOutputLocation(new File(output_fn)); 98 | queue.dumpToWriter(gtfsWriter); 99 | gtfsWriter.close(); 100 | System.out.println( "done" ); 101 | } 102 | 103 | private Map> groupFeatures(List extFeatures) { 104 | Map> ret = new HashMap>(); 105 | 106 | // gather by route id 107 | for( ExtendedFeature exft : extFeatures ){ 108 | String id = exft.getProperty( config.getRouteIdPropName() ); 109 | 110 | if (id == null) { 111 | id = "generated_" + nextId++; 112 | } 113 | 114 | 115 | List group = ret.get(id); 116 | if(group==null){ 117 | group = new ArrayList(); 118 | ret.put(id, group); 119 | } 120 | 121 | group.add(exft); 122 | } 123 | 124 | // order each group by segment 125 | for( List group : ret.values() ){ 126 | Collections.sort(group, new Comparator(){ 127 | 128 | @Override 129 | public int compare(ExtendedFeature o1, ExtendedFeature o2) { 130 | String segStr1 = o1.getProperty("segment"); 131 | Integer seg1; 132 | try{ 133 | seg1 = Integer.parseInt(segStr1); 134 | } catch (NumberFormatException ex){ 135 | seg1 = 0; 136 | } 137 | 138 | String segStr2 = o2.getProperty("segment"); 139 | Integer seg2; 140 | try{ 141 | seg2 = Integer.parseInt(segStr2); 142 | } catch (NumberFormatException ex){ 143 | seg2 = 0; 144 | } 145 | 146 | return seg1-seg2; 147 | } 148 | 149 | }); 150 | } 151 | 152 | return ret; 153 | } 154 | 155 | private List filterFeatures(List extFeatures) { 156 | List ret = new ArrayList(); 157 | 158 | for( ExtendedFeature exft : extFeatures ){ 159 | if (config.passesFilter(exft)) { 160 | ret.add(exft); 161 | } 162 | } 163 | 164 | return ret; 165 | } 166 | 167 | private List joinFeatures(List features, CsvJoinTable csvJoin) { 168 | List ret = new ArrayList(); 169 | 170 | for (Feature feat : features) { 171 | 172 | ExtendedFeature exft = new ExtendedFeature(feat, csvJoin); 173 | 174 | ret.add( exft ); 175 | } 176 | 177 | return ret; 178 | } 179 | 180 | private void featToGtfs(List group, Agency agency, 181 | StopGenerator stopGenerator, String routeId) throws Exception { 182 | 183 | ExtendedFeature exemplar = group.get(0); 184 | 185 | // get route type 186 | Integer mode = config.getMode(exemplar); 187 | 188 | // generate route 189 | String routeName = exemplar.getProperty(config.getRouteNamePropName()); 190 | 191 | if (routeName == null) 192 | routeName = routeId; 193 | 194 | System.out.println("generating elements for \"" + routeName + "\""); 195 | 196 | Route route = new Route(); 197 | route.setId(new AgencyAndId(DEFAULT_AGENCY_ID, routeId)); 198 | route.setShortName(routeName); 199 | route.setAgency(agency); 200 | route.setType(mode); 201 | queue.routes.add(route); 202 | 203 | List protoRoutes = new ArrayList(); 204 | for(ExtendedFeature exft : group){ 205 | // figure out spacing and speed for mode 206 | Double speed = config.getSpeed(exft); 207 | 208 | ProtoRoute protoroute = stopGenerator.makeProtoRoute(exft, speed); 209 | protoRoutes.add( protoroute ); 210 | } 211 | 212 | Map prsStops = new HashMap(); 213 | for(ProtoRoute protoroute : protoRoutes ){ 214 | for (ProtoRouteStop prs : protoroute.ret) { 215 | // generate stops 216 | Stop stop = prs.stop; 217 | 218 | if (!queue.stops.contains(stop)) 219 | queue.stops.add(stop); 220 | 221 | prsStops.put(prs, stop); 222 | } 223 | } 224 | 225 | if( !config.isExact() ){ 226 | makeFrequencyTrip(exemplar, protoRoutes, route, prsStops, false, config.usePeriods()); 227 | if (config.isBidirectional()) { 228 | makeFrequencyTrip(exemplar, protoRoutes, route, prsStops, true, config.usePeriods()); 229 | } 230 | } else { 231 | makeTimetableTrips(exemplar, protoRoutes, route, prsStops, false, config.usePeriods()); 232 | if (config.isBidirectional()) { 233 | makeTimetableTrips(exemplar, protoRoutes, route, prsStops, true, config.usePeriods()); 234 | } 235 | } 236 | 237 | } 238 | 239 | private void makeTimetableTrips(ExtendedFeature exft, List protoRoutes, Route route, 240 | Map prsStops, boolean reverse, boolean usePeriods) throws FeatureDoesntDefineTimeWindowException { 241 | // for each window 242 | for (ServiceWindow window : config.getServiceWindows()) { 243 | Double headway; 244 | try{ 245 | headway = getHeadway(exft, window.propName, usePeriods); 246 | } catch (FeatureDoesntDefineTimeWindowException ex){ 247 | System.out.println( "route id:"+route.getId().getId()+" has no value for time window "+window.propName ); 248 | if( config.tolerant() ){ 249 | continue; 250 | } else { 251 | throw ex; 252 | } 253 | } 254 | if(headway==null){ 255 | continue; 256 | } 257 | 258 | // generate a series of trips 259 | for(int t=window.startSecs(); t stopTimes = new ArrayList(); 266 | 267 | for(int i=0; i segStopTimes = createStopTimes(protoRoute.ret, prsStops, reverse, protoRoute.speed, trip, segStart, firstStopTimeSeq, protoRoute.length); 275 | stopTimes.addAll(segStopTimes); 276 | segStart += protoRoute.getDuration(); 277 | firstStopTimeSeq += segStopTimes.size(); 278 | } 279 | 280 | 281 | queue.stoptimes.addAll(stopTimes); 282 | } 283 | } 284 | 285 | } 286 | 287 | private void makeFrequencyTrip(ExtendedFeature exft, List protoRoutes, Route route, 288 | Map prsStops, boolean reverse, boolean usePeriods) throws FeatureDoesntDefineTimeWindowException { 289 | // generate a trip 290 | Trip trip = makeNewTrip(route, reverse); 291 | queue.trips.add(trip); 292 | 293 | // generate a frequency 294 | for (ServiceWindow window : config.getServiceWindows()) { 295 | Double headway; 296 | try{ 297 | headway = getHeadway(exft, window.propName, usePeriods); 298 | } catch (FeatureDoesntDefineTimeWindowException ex){ 299 | System.out.println( "feature for route id:"+route.getId().getId()+" does not define time window '"+window.propName+"'" ); 300 | if( config.tolerant() ){ 301 | continue; 302 | } else { 303 | throw ex; 304 | } 305 | } 306 | 307 | if (headway == null) 308 | continue; // no service in this time window 309 | 310 | headway /= config.waitFactor(); 311 | 312 | Frequency freq = makeFreq(headway, window.startSecs(), window.endSecs(), trip); 313 | queue.frequencies.add(freq); 314 | } 315 | 316 | int segStart = 0; 317 | int firstStopTimeSeq=0; 318 | List newStopTimes = new ArrayList(); 319 | for( ProtoRoute protoRoute : protoRoutes ){ 320 | List segStopTimes = createStopTimes(protoRoute.ret, prsStops, reverse, protoRoute.speed, trip, segStart, firstStopTimeSeq, protoRoute.length); 321 | newStopTimes.addAll(segStopTimes); 322 | segStart += protoRoute.getDuration(); 323 | firstStopTimeSeq += segStopTimes.size(); 324 | } 325 | 326 | queue.stoptimes.addAll(newStopTimes); 327 | 328 | } 329 | 330 | private Trip makeNewTrip(Route route, boolean reverse) { 331 | Trip trip = new Trip(); 332 | trip.setRoute(route); 333 | trip.setId(new AgencyAndId(DEFAULT_AGENCY_ID, String.valueOf(queue.trips.size()))); 334 | trip.setServiceId(new AgencyAndId(DEFAULT_AGENCY_ID, DEFAULT_CAL_ID)); 335 | if(reverse){ 336 | trip.setDirectionId("1"); 337 | } else { 338 | trip.setDirectionId("0"); 339 | } 340 | return trip; 341 | } 342 | 343 | private static List createStopTimes(List prss, Map prsStops, 344 | boolean reverse, double speed, Trip trip, int tripStart, int firstStopTimeSequence, double segLen) { 345 | List newStopTimes = new ArrayList(); 346 | for (int i = 0; i < prss.size(); i++) { 347 | 348 | int ix = i; 349 | if (reverse) { 350 | ix = prss.size() - 1 - i; 351 | } 352 | ProtoRouteStop prs = prss.get(ix); 353 | Stop stop = prsStops.get(prs); 354 | 355 | // generate stoptime 356 | StopTime stoptime = new StopTime(); 357 | stoptime.setStop(stop); 358 | stoptime.setTrip(trip); 359 | stoptime.setStopSequence(i+firstStopTimeSequence); 360 | 361 | double dist; 362 | if(reverse){ 363 | dist = segLen-prs.dist; 364 | } else { 365 | dist = prs.dist; 366 | } 367 | int time = (int) (dist / speed) + tripStart; 368 | 369 | stoptime.setArrivalTime(time); 370 | stoptime.setDepartureTime(time); 371 | 372 | newStopTimes.add(stoptime); 373 | 374 | } 375 | return newStopTimes; 376 | } 377 | 378 | private static Frequency makeFreq(double headway, int beginSecs, int endSecs, Trip trip) { 379 | Frequency freq; 380 | 381 | freq = new Frequency(); 382 | freq.setStartTime(beginSecs); 383 | freq.setEndTime(endSecs); 384 | 385 | freq.setHeadwaySecs((int) (headway)); 386 | 387 | freq.setTrip(trip); 388 | 389 | return freq; 390 | } 391 | 392 | private Double getHeadway(ExtendedFeature exft, String propName, boolean usePeriods) throws FeatureDoesntDefineTimeWindowException { 393 | double headway; 394 | String freqStr = exft.getProperty(propName); 395 | Double freqDbl; 396 | 397 | if (freqStr == null || freqStr.equals("None")) { 398 | freqDbl = config.getDefaultServiceLevel(); 399 | System.err.println("warning: using default frequency for feature " + exft.toString()); 400 | } else { 401 | freqDbl = Double.parseDouble(freqStr); 402 | } 403 | 404 | if (freqDbl == 0.0 || freqDbl == null) { 405 | throw new FeatureDoesntDefineTimeWindowException(propName); 406 | } 407 | 408 | if (usePeriods) { 409 | headway = freqDbl * 60; // minutes to seconds 410 | } else { 411 | headway = 3600 / freqDbl; 412 | } 413 | return headway; 414 | } 415 | 416 | static List getFeatures(String shp_filename) throws MalformedURLException, IOException { 417 | // construct shapefile factory 418 | File file = new File(shp_filename); 419 | Map map = new HashMap(); 420 | map.put("url", file.toURI().toURL()); 421 | DataStore dataStore = DataStoreFinder.getDataStore(map); 422 | 423 | // get shapefile as generic 'feature source' 424 | String typeName = dataStore.getTypeNames()[0]; 425 | SimpleFeatureSource featureSource = dataStore.getFeatureSource(dataStore.getTypeNames()[0]); 426 | 427 | SimpleFeatureType schema = featureSource.getSchema(); 428 | 429 | CoordinateReferenceSystem shpCRS = schema.getCoordinateReferenceSystem(); 430 | 431 | SimpleFeatureCollection collection = featureSource.getFeatures(); 432 | SimpleFeatureIterator iterator = collection.features(); 433 | 434 | List ret = new ArrayList(collection.size()); 435 | 436 | if (shpCRS != null && !shpCRS.equals(DefaultGeographicCRS.WGS84)) { 437 | try { 438 | MathTransform transform = CRS.findMathTransform(shpCRS, DefaultGeographicCRS.WGS84, true); 439 | 440 | while (iterator.hasNext()) { 441 | SimpleFeature next = iterator.next(); 442 | 443 | Geometry geom = (Geometry) next.getDefaultGeometry(); 444 | next.setDefaultGeometry(JTS.transform(geom, transform)); 445 | 446 | ret.add(next); 447 | } 448 | } catch (Exception e) { 449 | throw new RuntimeException(e); 450 | } 451 | } 452 | else { 453 | while (iterator.hasNext()) { 454 | ret.add(iterator.next()); 455 | } 456 | } 457 | 458 | return ret; 459 | } 460 | 461 | } 462 | -------------------------------------------------------------------------------- /src/main/java/com/conveyal/geom2gtfs/PicketStopGenerator.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.geom2gtfs; 2 | 3 | import org.json.JSONArray; 4 | import org.json.JSONObject; 5 | import org.opengis.feature.GeometryAttribute; 6 | 7 | import com.vividsolutions.jts.geom.Coordinate; 8 | import com.vividsolutions.jts.geom.LineString; 9 | import com.vividsolutions.jts.geom.MultiLineString; 10 | 11 | public class PicketStopGenerator implements StopGenerator { 12 | 13 | private static final boolean FAIL_ON_MULTILINESTRING = true; 14 | 15 | private JSONObject data; 16 | 17 | public PicketStopGenerator(JSONObject data) { 18 | this.data = data; 19 | } 20 | 21 | private ProtoRoute makeProtoRouteStopsFromLinestring(LineString geom, double spacing) { 22 | ProtoRoute ret = new ProtoRoute(); 23 | 24 | Coordinate[] coords = geom.getCoordinates(); 25 | double overshot = 0; 26 | double segStartDist = 0; 27 | 28 | double totalLen = 0; 29 | for (int i = 0; i < coords.length - 1; i++) { 30 | Coordinate p1 = coords[i]; 31 | Coordinate p2 = coords[i + 1]; 32 | 33 | double segCurs = overshot; 34 | double segLen = GeoMath.greatCircle(p1, p2); 35 | totalLen += segLen; 36 | 37 | while (segCurs < segLen) { 38 | double index = segCurs / segLen; 39 | Coordinate interp = GeoMath.interpolate(p1, p2, index); 40 | 41 | ProtoRouteStop prs = new ProtoRouteStop(interp, segStartDist + segCurs); 42 | ret.add(prs); 43 | 44 | segCurs += spacing; 45 | } 46 | 47 | overshot = segCurs - segLen; 48 | 49 | segStartDist += segLen; 50 | } 51 | 52 | // add one final stop, at the very end 53 | ProtoRouteStop prs = new ProtoRouteStop(coords[coords.length - 1], totalLen); 54 | ret.add(prs); 55 | 56 | ret.length = totalLen; 57 | 58 | return ret; 59 | } 60 | 61 | public ProtoRoute makeProtoRoute(ExtendedFeature exft, Double speed) throws Exception { 62 | GeometryAttribute geomAttr = exft.feat.getDefaultGeometryProperty(); 63 | MultiLineString geom = (MultiLineString) geomAttr.getValue(); 64 | 65 | if (FAIL_ON_MULTILINESTRING && geom.getNumGeometries() > 1) { 66 | throw new Exception("Features may only contain a single linestring."); 67 | } 68 | 69 | double spacing = Config.getSpacing(exft, this.data); 70 | 71 | LineString ls = (LineString) geom.getGeometryN(0); 72 | ProtoRoute ret = this.makeProtoRouteStopsFromLinestring(ls, spacing); 73 | ret.speed = speed; 74 | return ret; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/conveyal/geom2gtfs/ProtoRoute.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.geom2gtfs; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class ProtoRoute { 7 | List ret = new ArrayList(); 8 | public double length; 9 | public double speed; 10 | 11 | public void add(ProtoRouteStop prs) { 12 | ret.add(prs); 13 | } 14 | 15 | public double getDuration() { 16 | return length/speed; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/conveyal/geom2gtfs/ProtoRouteStop.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.geom2gtfs; 2 | 3 | import org.onebusaway.gtfs.model.AgencyAndId; 4 | import org.onebusaway.gtfs.model.Stop; 5 | 6 | import com.vividsolutions.jts.geom.Coordinate; 7 | 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | public class ProtoRouteStop { 11 | // TODO: this is kind of hacky, we should really be using a separate ID space for each feed. 12 | private static AtomicInteger createdStopId = new AtomicInteger(); 13 | 14 | public ProtoRouteStop (Coordinate coord, double dist) { 15 | this.coord = coord; 16 | this.dist = dist; 17 | this.stop = new Stop(); 18 | stop.setLat(coord.y); 19 | stop.setLon(coord.x); 20 | int stopId = createdStopId.incrementAndGet(); 21 | stop.setName("Stop " + stopId); 22 | stop.setId(new AgencyAndId(Main.DEFAULT_AGENCY_ID, "created_stop_" + stopId)); 23 | } 24 | 25 | public ProtoRouteStop (Stop stop, double dist) { 26 | this.stop = stop; 27 | this.coord = new Coordinate(stop.getLon(), stop.getLat()); 28 | this.dist = dist; 29 | } 30 | 31 | public Coordinate coord; 32 | public double dist; 33 | public Stop stop; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/conveyal/geom2gtfs/ServiceWindow.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.geom2gtfs; 2 | 3 | public class ServiceWindow { 4 | 5 | private int start; 6 | private int end; 7 | public String propName; 8 | public int startSecs() { 9 | return start*3600; 10 | } 11 | public int endSecs(){ 12 | return end*3600; 13 | } 14 | public void setStartHour(int startHour) { 15 | this.start = startHour; 16 | } 17 | public void setEndHour(int endHour){ 18 | this.end = endHour; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/conveyal/geom2gtfs/ShapefileStopGenerator.java: -------------------------------------------------------------------------------- 1 | package com.conveyal.geom2gtfs; 2 | 3 | import java.io.IOException; 4 | import java.net.MalformedURLException; 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | import java.util.Comparator; 8 | import java.util.List; 9 | 10 | import org.geotools.data.FeatureSource; 11 | import org.geotools.feature.FeatureCollection; 12 | import org.geotools.feature.FeatureIterator; 13 | import org.geotools.referencing.GeodeticCalculator; 14 | import org.geotools.referencing.datum.DefaultEllipsoid; 15 | import org.json.JSONObject; 16 | import org.opengis.feature.Feature; 17 | import org.opengis.feature.GeometryAttribute; 18 | 19 | import com.vividsolutions.jts.geom.Coordinate; 20 | import com.vividsolutions.jts.geom.Geometry; 21 | import com.vividsolutions.jts.geom.LineString; 22 | import com.vividsolutions.jts.geom.MultiLineString; 23 | import com.vividsolutions.jts.linearref.LinearLocation; 24 | import com.vividsolutions.jts.linearref.LocationIndexedLine; 25 | 26 | public class ShapefileStopGenerator implements StopGenerator { 27 | 28 | static boolean FAIL_ON_MULTILINESTRING = true; 29 | 30 | double threshold; 31 | List stops; 32 | 33 | GeodeticCalculator gc = new GeodeticCalculator(DefaultEllipsoid.WGS84); 34 | 35 | public ShapefileStopGenerator(JSONObject data) throws MalformedURLException, IOException { 36 | String filename = data.getString("filename"); 37 | threshold = data.getDouble("threshold"); 38 | 39 | // collect all features from shapefile 40 | stops = Main.getFeatures(filename); 41 | } 42 | 43 | @Override 44 | public ProtoRoute makeProtoRoute(ExtendedFeature exft, Double speed) throws Exception { 45 | GeometryAttribute geomAttr = exft.feat.getDefaultGeometryProperty(); 46 | MultiLineString geom = (MultiLineString) geomAttr.getValue(); 47 | 48 | if (FAIL_ON_MULTILINESTRING && geom.getNumGeometries() > 1) { 49 | throw new Exception("Features may only contain a single linestring."); 50 | } 51 | 52 | LineString ls = (LineString) geom.getGeometryN(0); 53 | ProtoRoute ret = this.makeProtoRouteStopsFromLinestring(ls); 54 | ret.speed = speed; 55 | 56 | return ret; 57 | } 58 | 59 | private ProtoRoute makeProtoRouteStopsFromLinestring(LineString ls) { 60 | LocationIndexedLine ils = new LocationIndexedLine(ls); 61 | 62 | // create buffer of linestring 63 | Geometry buffer = ls.buffer( threshold ); // note distance is in same units as geoemtry 64 | 65 | // get all features in stop shapefile that fall within buffer 66 | List nearbyStops = new ArrayList(); 67 | for(Feature stop : stops){ 68 | Geometry stopGeom = (Geometry)stop.getDefaultGeometryProperty().getValue(); 69 | if(stopGeom.within(buffer)){ 70 | nearbyStops.add(stop); 71 | } 72 | } 73 | 74 | // for each feature, reference along the the linestring 75 | List prss = new ArrayList(); 76 | for(Feature feature : nearbyStops ){ 77 | Geometry geom = (Geometry) feature.getDefaultGeometryProperty().getValue(); 78 | LinearLocation ix = ils.project(geom.getCoordinate()); 79 | ProtoRouteStop prs = generateProtoRouteStop( ls, ix ); 80 | prss.add(prs); 81 | } 82 | 83 | // sort linearly along linestring 84 | Collections.sort(prss, new Comparator(){ 85 | 86 | @Override 87 | public int compare(ProtoRouteStop o1, ProtoRouteStop o2) { 88 | if(o2.dist>o1.dist){ 89 | return -1; 90 | } else if(o1.dist>o2.dist){ 91 | return 1; 92 | } else { 93 | return 0; 94 | } 95 | } 96 | 97 | }); 98 | 99 | // copy to return structure while removing duplicates 100 | ProtoRoute ret = new ProtoRoute(); 101 | double lastDist=-1; 102 | for(ProtoRouteStop prs : prss){ 103 | if(prs.dist != lastDist){ 104 | ret.add(prs); 105 | lastDist = prs.dist; 106 | } 107 | } 108 | 109 | ret.length = distAlongLineString( ls, ils.getEndIndex() ); 110 | 111 | return ret; 112 | } 113 | 114 | private ProtoRouteStop generateProtoRouteStop(LineString ls, LinearLocation ix) { 115 | double dist = distAlongLineString( ls, ix ); 116 | 117 | ProtoRouteStop prs = new ProtoRouteStop(ix.getCoordinate(ls), dist); 118 | 119 | return prs; 120 | } 121 | 122 | private double distAlongLineString(LineString ls, LinearLocation ix) { 123 | double dist=0; 124 | 125 | int seg = ix.getSegmentIndex(); 126 | 127 | double segDist=0; 128 | for(int i=0; i