[JAVA] I tried to make my own transfer guide using OpenTripPlanner and GTFS

Introduction

I created my own transfer information site (European Subcontinent Railway) using OpenTripPlanner (hereinafter, OTP) as a route search engine, but only with OTP as a route search engine. It seems that there is not much Japanese information on how to use it (although there is not much information on English ...), so I summarized it as an article. Also, since only the timetable data is registered without registering the map data, please use it as a reference for such usage.

What is OpenTripPlanner?

OpenTripPlanner is an open source integrated route search application. It is possible to take in OpenStreetMap and GTFS (described later) data and perform route search including walking and vehicles, so-called transfer guidance.

What is GTFS?

GTFS is a format for public transportation routes and operation information created by Google. In short, it is timetable data, and in recent years, transportation data around the world has been released as open data.

Introduction of the created site European Subcontinent Railway

Before the war (around 1939), when there were no jet planes, it seems that one ticket could be used to travel by rail and ship from Japan to Europe. We are creating a hobby site that is particularly useless and can provide information on train transfers in such times. The timetables of railways from all over the world are collected, and the data of the railway trunk lines from Tokyo to London and Paris (Japan → Korea → former Manchuria → former Soviet Union → Poland → Germany → Belgium → UK / France) are registered. image.png

How to use OTP

For the method of launching OTP as a normal web application, refer to other articles, but this time I will explain specifically for the following.

--Use OTP as a Java library instead of an application --Register and use only GTFS data without registering map data in OTP

Preparation

The following version of the environment is assumed.

--JDK 1.8 or above

Create a Maven project and add the following information to the pom.xml file. I'm adding the repository because OTP references another OSS called OneBusAway and GeoTools.

pom.xml


<repositories>
  <repository>
    <id>onebusaway-releases</id>
    <name>Onebusaway Releases Repo</name>
    <url>http://nexus.onebusaway.org/content/repositories/releases/</url>
  </repository>
  <repository>
    <id>osgeo</id>
    <name>OSGeo Release Repository</name>
    <url>https://repo.osgeo.org/repository/release/</url>
  </repository>
</repositories>

<dependencies>
  <dependency>
    <groupId>org.opentripplanner</groupId>
    <artifactId>otp</artifactId>
    <version>1.4.0</version>
  </dependency>
</dependencies>

Creating Graph

OTP stores road and transportation routes and timetable data in an object called Graph. However, before creating a Graph object, you need to read the zipped GTFS data and create a GtfsBundle object. Since OTP can read multiple GTFS files, it is necessary to give each file a unique ID. (The part where feedId is set)

python


GtfsBundle gtfsBundle = new GtfsBundle(file);
gtfsBundle.setTransfersTxtDefinesStationPaths(true);
GtfsFeedId feedId = new GtfsFeedId.Builder().id("id").build();
gtfsBundle.setFeedId(feedId);

After merging the created GtfsBundle into one List (gtfsBundles), register it in the Graph object.

python


GtfsModule gtfsModule = new GtfsModule(gtfsBundles);
Graph graph = new Graph();
gtfsModule.buildGraph(graph, null);
graph.summarizeBuilderAnnotations();
graph.index(new DefaultStreetVertexIndexFactory());

Route search

First, create a RoutingRequest object that sets search conditions such as departure place, arrival place, and departure (or arrival) time. Since it is originally a route search application that uses map data, it is necessary to specify the coordinates even at the station.

python


RoutingRequest routingRequest = new RoutingRequest();
routingRequest.setNumItineraries(3); //Number of candidates for search results
//Set the reference time to search
LocalDateTime ldt = LocalDateTime.parse("1939-08-01T13:00");
routingRequest.dateTime = ldt.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
routingRequest.setArriveBy(false); //The default is to specify the departure time, but if True, you can specify the arrival time.
//Latitude of departure and arrival/Specified by longitude
routingRequest.from = new GenericLocation(35.3369, 139.44699); //Tsujido Station
routingRequest.to = new GenericLocation(35.17096, 136.88232); //Nagoya station
routingRequest.ignoreRealtimeUpdates = true; //Ignore GTFS Realtime information
routingRequest.reverseOptimizing = true; //Search in the opposite direction from the search results and search for the latest departure time
//Setting to search only with GTFS data
routingRequest.setModes(new TraverseModeSet(TraverseMode.TRANSIT));
routingRequest.onlyTransitTrips = true;
//Set Graph object
routingRequest.setRoutingContext(graph);

Search using RoutingReques object and Graph object.

python


Router router = new Router("OTP", graph);
List<GraphPath> paths = new GraphPathFinder(router).getPaths(routingRequest);
TripPlan tripPlan = GraphPathToTripPlanConverter.generatePlan(paths, routingRequest);

The search result is stored in the TripPlan object. The candidate Itinerary object of the search result is stored in the List obtained by the itinerary property of TripPlan. The Leg object of the route information is stored in the list obtained by the leg property of Itinerary in the order of transfer.

Summary

Search results

August 1939 using GTFS data (timetable data of the Tokaido Line in December 1939, about 80 years ago) on my site European Subcontinent Railway Let's explore the route from Tsujido Station to Nagoya Station at 1:00 pm on the 1st. The result is as follows. If you miss the train at 13:08 (by the way, the regular train bound for Maibara), it seems that it is faster to return to Yokohama and Ofuna and catch the limited express Sakura Shimonoseki and express Osaka. You can check the timetable of each station in 1939 by clicking the station name on each route map in European Subcontinent Railway. (Tsujido Station, Yokohama Station, Ofuna Station)

13:08 Departure- 18:28 arrival(320 minutes)
>Tsujido 13:08 departure-Numazu 14:46 arrivals
>Numazu 15:04 departures-Nagoya 18:28 arrivals
13:18 Departure- 19:00 arrival(342 minutes)
>Tsujido 13:18 shots-Yokohama 13:50 arrivals
>Yokohama 13:58 departures-Nagoya 19:00 arrival
14:08 Departure- 19:38 arrival(330 minutes)
>Tsujido 14:08 departure-Ofuna 14:20 arrivals
>Ofuna 14:26 shots-Nagoya 19:38 arrivals

All source code

The full text of this source looks like this.

OtpTest.java


import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Calendar;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.opentripplanner.api.model.TripPlan;
import org.opentripplanner.api.resource.GraphPathToTripPlanConverter;
import org.opentripplanner.common.model.GenericLocation;
import org.opentripplanner.graph_builder.model.GtfsBundle;
import org.opentripplanner.graph_builder.module.GtfsFeedId;
import org.opentripplanner.graph_builder.module.GtfsModule;
import org.opentripplanner.model.FeedScopedId;
import org.opentripplanner.model.Route;
import org.opentripplanner.model.Stop;
import org.opentripplanner.model.Trip;
import org.opentripplanner.routing.core.RoutingRequest;
import org.opentripplanner.routing.core.TraverseMode;
import org.opentripplanner.routing.core.TraverseModeSet;
import org.opentripplanner.routing.edgetype.TripPattern;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.graph.GraphIndex;
import org.opentripplanner.routing.impl.DefaultStreetVertexIndexFactory;
import org.opentripplanner.routing.impl.GraphPathFinder;
import org.opentripplanner.routing.spt.GraphPath;
import org.opentripplanner.standalone.Router;

public class OtpTest {

	public static void main(String[] args) {

		//Path to the GTFS file storage folder
		String in = "";

		//Read all ZIP files in the folder as GTFS files
		List<GtfsBundle> gtfsBundles = null;
		try (final Stream<Path> pathStream = Files.list(Paths.get(in))) {
			gtfsBundles = pathStream.map(Path::toFile).filter(file -> file.getName().toLowerCase().endsWith(".zip"))
					.map(file -> {
						GtfsBundle gtfsBundle = new GtfsBundle(file);
						gtfsBundle.setTransfersTxtDefinesStationPaths(true);
						//Since it is necessary to give each GTFS file a unique ID, use the file name as the ID.
						String id = file.getName().substring(0, file.getName().length() - 4);
						gtfsBundle.setFeedId(new GtfsFeedId.Builder().id(id).build());
						return gtfsBundle;
					}).collect(Collectors.toList());
		} catch (final IOException e) {
			throw new RuntimeException(e);
		}

		//Processing to register the read GTFS file in the Graph object
		GtfsModule gtfsModule = new GtfsModule(gtfsBundles);
		Graph graph = new Graph();
		gtfsModule.buildGraph(graph, null);
		graph.summarizeBuilderAnnotations();
		graph.index(new DefaultStreetVertexIndexFactory());
		
		//Graph can also be serialized and saved
		// graph.save(file);

		//Setting search conditions
		RoutingRequest routingRequest = new RoutingRequest();
		routingRequest.setNumItineraries(3); //Number of candidates for search results
		//Set the reference time to search
		LocalDateTime ldt = LocalDateTime.parse("1939-08-01T13:00");
		routingRequest.dateTime = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()).getTime() / 1000;
		routingRequest.setArriveBy(false); //The default is to specify the departure time, but if True, you can specify the arrival time.
		//Latitude of departure and arrival/Specified as mild
		routingRequest.from = new GenericLocation(35.3369, 139.44699); //Tsujido Station
		routingRequest.to = new GenericLocation(35.17096, 136.88232); //Nagoya station
		routingRequest.ignoreRealtimeUpdates = true; //Ignore GTFS Realtime information
		routingRequest.reverseOptimizing = true; //Search in the opposite direction from the search results and search for the latest departure time
		//Setting to search only with GTFS data
		routingRequest.setModes(new TraverseModeSet(TraverseMode.TRANSIT));
		routingRequest.onlyTransitTrips = true;
		//Set Graph object
		routingRequest.setRoutingContext(graph);

		//Search processing
		Router router = new Router("OTP", graph);
		List<GraphPath> paths = new GraphPathFinder(router).getPaths(routingRequest);
		TripPlan tripPlan = GraphPathToTripPlanConverter.generatePlan(paths, routingRequest);

		//Display search results
		tripPlan.itinerary.forEach(p -> {
			//Summary information for each route candidate
			System.out.println(String.format("%d:%02d Departure- %d:%02d arrival(%d minutes)", 
					p.startTime.get(Calendar.HOUR_OF_DAY), p.startTime.get(Calendar.MINUTE), 
					p.endTime.get(Calendar.HOUR_OF_DAY), p.endTime.get(Calendar.MINUTE), 
					(p.duration / 60)));
			p.legs.forEach(l -> {
				//Information on each train in the route
				System.out.println(String.format("> %s %d:%02d departure- %s %d:%02d arrival", 
						l.from.name, l.startTime.get(Calendar.HOUR_OF_DAY), l.startTime.get(Calendar.MINUTE), 
						l.to.name, l.endTime.get(Calendar.HOUR_OF_DAY), l.endTime.get(Calendar.MINUTE)));
			});
		});
	}
}

Also, from the Leg object, if the route drawing information is stored in GTFS shapes.txt, it can be obtained as a GEO-encoded character string with l.legGeometry.getPoints ().

Recommended Posts

I tried to make my own transfer guide using OpenTripPlanner and GTFS
I created and set my own Dialect with Thymeleaf and tried using it
I tried using Hotwire to make Rails 6.1 scaffold a SPA
I tried to make Java Optional and guard clause coexist
[Rails] I tried to implement "Like function" using rails and js
I tried to summarize object orientation in my own way.
I tried to integrate Docker and Maven / Netbean nicely using Jib
[Unity] I tried to make a native plug-in UniNWPathMonitor using NWPathMonitor
I tried to make it an arbitrary URL using routing nesting
I tried to make a simple face recognition Android application using OpenCV
I made a virtual currency arbitrage bot and tried to make money
I tried to make a talk application in Java using AI "A3RT"
I tried to make Basic authentication with Java
A story when I tried to make a video by linking Processing and Resolume
[Android] I quit SQLite and tried using Realm
I tried to link JavaFX and Spring Framework.
I tried to implement a server using Netty
[JDBC ③] I tried to input from the main method using placeholders and arguments.
I tried using Wercker to create and publish a Docker image that launches GlassFish 5.
I tried to operate SQS using AWS Java SDK
I started MySQL 5.7 with docker-compose and tried to connect
I tried to integrate AWS I oT button and Slack
I tried to make a login function in Java
I tried using YOLO v4 on Ubuntu and ROS
I tried to make FizzBuzz that is uselessly flexible
I tried to chew C # (reading and writing files)
I tried to build an environment using Docker (beginner)
I tried using Gson
I tried using TestNG
I tried using Galasa
I was trapped when I generated my own class equals and hashCode in Java using IDE
I tried to make an app that allows you to post and chat by genre ~ App overview ~
Easy to make LINE BOT with Java Servlet Part 2: I tried image messages and templates
I tried unit testing Rails app using RSpec and FactoryBot
I tried to make an application in 3 months from inexperienced
I tried to make an introduction to PHP + MySQL with Docker
I tried to summarize the basics of kotlin and java
I tried to make Venn diagram an easy-to-understand GIF animation
I tried to verify this and that of Spring @ Transactional
I want to make a list with kotlin and java!
I want to make a function with kotlin and java!
I tried to introduce UI animation to Pokedex using Poké API
I tried to build the environment little by little using docker
I tried to summarize personally useful apps and development tools (development tools)
I tried to summarize personally useful apps and development tools (Apps)
I tried using Dapr in Java to facilitate microservice development
I tried to make a client of RESAS-API in Java
I tried to get started with Swagger using Spring Boot
[Rails] Implementation of multi-layer category function using ancestry "I tried to make a window with Bootstrap 3"
[Spring Boot] I want to add my own property file and get the value with env.getProperty ().
After learning Progate, I tried to make an SNS application using Rails in the local environment
When I tried to run my own service, it failed, so I screwed it into the task scheduler
I tried using azure cloud-init
I tried using Apache Wicket
I tried using Java REPL
I tried to verify yum-cron
[Metal] I tried to figure out the flow until rendering using Metal
I just want to write Java using Eclipse on my Mac
I tried connecting to MySQL using JDBC Template with Spring MVC
I tried to deepen my understanding of object orientation by n%
I tried to build a simple application using Dockder + Rails Scaffold