Skip to content

A multimodal journey planner that is built on open source components and is powered by open data.

License

Notifications You must be signed in to change notification settings

langbein-daniel/BikeTripPlanner

Repository files navigation

BikeTripPlanner

A multimodal journey planner that is built on open source components and is powered by open data.

Configuration for another region with different data sources is possible through a single .env file. Using Docker Compose, the project can be run locally or publicly accessible through a domain with automatic HTTPS certificate generation.

The core components are Digitransit UI (web frontend), OpenTripPlanner (multimodal router), Pelias (geocoder), tilemaker (map generation), Tileserver GL (map server) and nginx-proxy (reverse proxy and HTTPS certificates). Data sources are a GTFS feed (for scheduled public transit data), OpenStreetmap (for road network and address data) and Who's On First (for places and their hierarchy).

diagram-architecture.svg

TLDR:

  • Configuration is done in .env.
  • Run make build to build all Docker images.
  • Thereafter, run make test to check if the built containers start healthy or run make start to keep the local instance running.
  • Optionally, use make publish to upload the Docker images into a registry.
  • Lastly, see Deployment for detains on making your BikeTripPlanner instance publicly available under a domain and with HTTPS certificates.

Demo

A demo instance covering the area of the German transport association VGN is available at https://biketripplanner.de/.

Configuration

Go through .env and adjust the values as desired.

Important parts are:

  • Bounding box describing a rectangular geographical area.
  • Link to OpenStreetMap region covering the bounding box.
  • Link to GTFS feed providing transit data.
  • Requests to the individual services that are used for Docker container healthchecks. If the OpenStreetMap or GTFS data sources are changed, these need to be adjusted as well.

In addition to the default configuration for the area of the VGN transport association, there is a .env file for Finland. Just overwrite .env with it to give it a try.

For advanced configuration, see:

Build data images

Prerequisites: Install docker compose, jq, sudo and optionally make.

The following sections provide additional information about the individual build steps. There is also a Makefile to accomplish the same.

GTFS data

1) gtfs-data

Download the GTFS dataset:

sudo docker compose -f build-data.yml build --progress=plain --pull gtfs-data

2) gtfs-modified

If desired, the GTFS feed can be modified: Add or change the bikes_allowed column, delete files from the GTFS feed or fix unescaped double-quotes.

sudo docker compose -f build-data.yml build --progress=plain gtfs-modified

This step is optional and kan be skipped with:

sudo docker tag build-gtfs-data build-gtfs-modified

3) gtfs-filter

If desired, the GTFS feed can be filtered to the bounding box: Only routes (and all linked data) that reside inside the bounding box or intersect with it are kept.

sudo docker compose -f build-data.yml build --progress=plain gtfs-filtered

This step is optional and kan be skipped with:

sudo docker tag build-gtfs-modified build-gtfs-filtered

OSM data

1) osm-data

sudo docker compose -f build-data.yml build --progress=plain --pull osm-data

2) osm-excerpt

sudo docker compose -f build-data.yml build --progress=plain --pull osmium-tool
sudo docker compose -f build-data.yml build --progress=plain osm-excerpt

3) osm-filtered

sudo docker compose -f build-data.yml build --progress=plain --pull osmium-tool
sudo docker compose -f build-data.yml build --progress=plain osm-filtered

DEM data

Download DEM tiles:

sudo docker compose -f build-data.yml build --progress=plain --pull dem-data

Merge tiles into one DEM file:

sudo docker compose -f build-data.yml build --progress=plain dem-merged

Background map (Tileserver GL)

sudo docker compose -f build-tilemaker.yml build --progress=plain --pull tilemaker
# Don't `--pull` as we are using the previously built `tilemaker`.
sudo docker compose build --progress=plain tileserver-gl

Routing (OpenTripPlanner)

# Build final OpenTripPlanner container.
# Don't `--pull` as we are using the previously built `osm-excerpt`.
sudo docker compose build --progress=plain opentripplanner

Geocoder (Pelias)

# Set shell variables from `.env`.
export "$(grep '^BUILD_NAME=' < .env)"
export "$(grep '^PELIAS_BUILD_DIR=' < .env)"
export "$(grep '^COUNTRY_CODE=' < .env)"
export "$(grep '^WOF_IDS=' < .env)"

# Change `countryCode` in pelias.json file.
pelias_json="$(cat "${PELIAS_BUILD_DIR}/pelias.json")"
if [ "${COUNTRY_CODE}" = "" ]; then
  # Delete `countryCode`
  jq 'del(.imports.whosonfirst.countryCode)' <<< "${pelias_json}" > "${PELIAS_BUILD_DIR}/pelias.json"
else
  # Set `countryCode`
  jq ". | .imports.whosonfirst.countryCode=\"${COUNTRY_CODE}\"" <<< "${pelias_json}" > "${PELIAS_BUILD_DIR}/pelias.json"
fi

# Change `importPlace` in pelias.json file.
pelias_json="$(cat "${PELIAS_BUILD_DIR}/pelias.json")"
if [ "${WOF_IDS}" = "" ]; then
  # Delete `importPlace`
  jq 'del(.imports.whosonfirst.importPlace)' <<< "${pelias_json}" > "${PELIAS_BUILD_DIR}/pelias.json"
else
  # Set `importPlace`
  jq --argjson wof_ids "${WOF_IDS}" '. | .imports.whosonfirst.importPlace=$wof_ids' <<< "${pelias_json}" > "${PELIAS_BUILD_DIR}/pelias.json"
fi

# Create temporary Pelias data directory.
mkdir -p "${PELIAS_BUILD_DIR}/data/"{elasticsearch,openstreetmap,gtfs}
# Start Elasticsearch and wait until healthy.
sudo docker compose -f build-pelias.yml up -d --wait elasticsearch
# Initialize Elasticsearch.
sudo docker compose -f build-pelias.yml run --rm schema ./bin/create_index
# Download, prepare and import data:
sudo docker run --rm --entrypoint cat ${BUILD_NAME}-osm-excerpt /data/osm.pbf > "${PELIAS_BUILD_DIR}/data/openstreetmap/osm.pbf"
sudo docker run --rm --entrypoint cat ${BUILD_NAME}-gtfs-filtered   /data/gtfs.zip        > "${PELIAS_BUILD_DIR}/data/gtfs/gtfs.zip"
sudo docker compose -f build-pelias.yml run --rm whosonfirst   ./bin/download

# The Pelias Docker Compose helper script uses the following command:
#   sudo docker compose -f build-pelias.yml run --rm polylines     ./docker_extract.sh
# But this complains about a too large OSM file (e.g. of whole Germany).
# Thus, we use another tool to generate polylines:
sudo docker compose -f build-pelias.yml build --pull polylines-gen
sudo docker compose -f build-pelias.yml run --rm polylines-gen

sudo docker compose -f build-pelias.yml run --rm placeholder   ./cmd/extract.sh
sudo docker compose -f build-pelias.yml run --rm placeholder   ./cmd/build.sh
sudo docker compose -f build-pelias.yml run --rm interpolation ./docker_build.sh
sudo docker compose -f build-pelias.yml run --rm whosonfirst   ./bin/start
sudo docker compose -f build-pelias.yml run --rm openstreetmap ./bin/start
sudo docker compose -f build-pelias.yml run --rm polylines     ./bin/start
sudo docker compose -f build-pelias.yml build --pull gtfs
sudo docker compose -f build-pelias.yml run --rm gtfs          ./bin/start

# Stop and remove intermediate containers.
sudo docker compose -f build-pelias.yml down
# Build final Pelias containers.
sudo docker compose build --progress=plain api libpostal placeholder interpolation pip elasticsearch
# Remove temporary data directory.
#sudo rm -r "${PELIAS_BUILD_DIR}/data"

License (Open Data Sources)

The source code of this project is licensed under the Clear BSD License, see LICENSE. The licenses of used software dependencies and data sources are different.

License information of (some of the) used open data sources can be collected with:

sudo docker compose -f build-data.yml build --progress=plain credits

The resulting credits.json file can be viewed with:

sudo docker compose -f build-data.yml run --rm credits cat /data/credits.json

For Digitransit UI to correctly credit used open data sources, the variable DATA_SOURCES_PARAGRAPHS has to be updated in .env. The new value can be printed with:

printf "'"
sudo docker compose -f build-data.yml run --rm credits cat /data/credits.json | jq '[.[].text]' | sed "s|'|\\\'|" | jq -c -j '.[] |= sub("(?<x>https://[^ ]+)"; "<a href=\"\(.x)\">\(.x)</a>")'
printf "'\n"

Web UI (Digitransit-UI)

sudo docker compose build --progress=plain --pull digitransit-ui

Test on local machine

Startup

Start all services and wait for them to be healthy:

sudo docker compose up -d --wait

If this fails, you may need to adjust the healthcheck configuration.

View healthcheck output

If a container is unhealthy, the first 4096 bytes of the healthcheck output can be viewed with:

CONTAINER=libpostal && \
CONTAINER_ID="$(sudo docker compose ps -q "${CONTAINER}")" && \
sudo docker inspect "${CONTAINER_ID}" | jq '.[].State.Health.Log[].Output'

Additional information: https://docs.docker.com/engine/reference/builder/#healthcheck

Interactive testing

In addition to the container healthchecks, you can do the following:

Shutdown

sudo docker compose down

Deployment

Publish images

Tag and push the locally built images to a docker container registry:

./publish.sh

Run with Docker Compose

In deployment, there are two Docker Compose examples.

Nginx reverse proxy with Let's Encrypt certificates

This example includes an nginx server that receives HTTPS requests from the Internet and proxies them to the corresponding services via HTTP. Certificate creation and renewal is automated using Let's Encrypt.

                                        Docker Compose
                               ┌──────────────────────────────┐
                               │                              │
                               │            ┌─►Tileserver GL  │
┌─────────┐                    │            │                 │
│         │HTTPS          HTTPS│        HTTP├─►OpenTripPlanner│
│ Client◄─┼─────►Internet◄─────┼─►nginx◄────┤                 │
│         │                    │            ├─►Pelias API     │
└─────────┘                    │            │                 │
                               │            └─►Digitransit-UI │
                               │                              │
                               └──────────────────────────────┘

Create one domain for the UI and three subdomains for the background map, geocoder and routing services. All four domains have to point to the server intended for running BikeTripPlanner. Also, make sure that the ports 80 and 443 are opened and reachable over the Internet.

Set the domain values in deployment/.env accordingly.

Then start your BikeTripPlanner instance with:

cd deployment
sudo docker compose -f btp-and-proxy.yml up -d --wait

Load distribution

As the Pelias and OpenTripPlanner services are quite RAM and CPU intensive, one might want to run some of the Docker containers on different servers to spread the load or even introduce load balancing of e.g. routing requests over multiple OpenTripPlanner instances.

This second example gives an overview how the large Docker Compose project can be split up into smaller parts, each of which run on a different server.

Separating the background map, routing, geocoding and UI services is as easy as copying the corresponding services from the deployment/btp-and-proxy.yml Docker Compose file into separate files and removing the depends_on statements of services that are no longer part of the same Docker Compose file. It is thereafter the users responsibility to start the Docker Compose projects in the correct order (e.g. starting the UI project last).

As we want to keep this example simple, we leave it up to the user which of the services named above they want to separate and keep them all in one Docker Compose project. But we lay the foundation to fine granular load distribution and load balancing by introducing a dedicated reverse proxy server which removes the CPU load of encrypting and decrypting HTTPS connections away from the BikeTripPlanner services.

                                                       Docker Compose
                                              ┌──────────────────────────────┐
                                              │                              │
                              Docker Compose  │            ┌─►Tileserver GL  │
┌─────────┐                    ┌─────────┐    │            │                 │
│         │HTTPS          HTTPS│         │HTTP│        HTTP├─►OpenTripPlanner│
│ Client◄─┼─────►Internet◄─────┼─►nginx◄─┼────┼─►nginx◄────┤                 │
│         │                    │         │    │            ├─►Pelias API     │
└─────────┘                    └─────────┘    │            │                 │
                                              │            └─►Digitransit-UI │
                                              │                              │
                                              └──────────────────────────────┘

Create one domain for the UI and three subdomains for the background map, geocoder and routing services. All four domains have to point to the server intended for running the reverse proxy. Also, make sure that the ports 80 and 443 are open and reachable over the Internet on that server.

Set the domain values in deployment/.env accordingly.

Set the IP of the BikeTripPlanner server in deployment/.env for all four BikeTripPlanner services. (If you run e.g. OpenTripPlanner separately from the other BikeTripPlanner services, then set IP_OPENTRIPPLANNER to a different IP.)

On your reverse proxy server:

cd deployment
sudo docker compose -f proxy-only.yml up -d --wait

On your BikeTripPlanner server:

cd deployment
sudo docker compose -f btp-only.yml up -d --wait

Other notes

Extract file from Docker image.

Example: Extract GTFS zip file after the gtfs-modified build step:

export "$(grep '^BUILD_NAME=' < .env)"
sudo docker run --rm --entrypoint cat ${BUILD_NAME}-gtfs-modified /data/gtfs.zip > gtfs.zip

Similar Projects

Feature matrix

Mode OTP UI Digitransit UI MOTIS Demo Navitia API Bayern Fahrplan
"Bike" yes yes no no no
"Bike" -> "Transit" yes yes yes (1) yes yes
"Transit" -> "Bike" yes yes yes (1) yes yes
"Bike" -> "Transit" -> "Bike" ? no yes (1) yes yes
"Bike and Transit" (2) yes yes no no no

(1): In the UI of the demo, at 30 minutes of bicycle duration can be selected. The API documentation does not mention an upper limit: https://motis-project.de/docs/api/endpoint/intermodal.html

(2): MOTIS, Navitia and Bayern Fahrplan allow to search for journeys with bike trip legs before and after the public transit part in the middle which is transit-only (Trips where public transit is reached or left by bike). OpenTripPlanner (OTP) has a multimodal mode combining cycling with public transit ("Bike and Transit") where at any part of a journey bike trip legs can occur.

Routing options & other features OTP UI Digitransit UI MOTIS Demo Navitia API Bayern Fahrplan
Bike speed yes yes no yes no
Vehicle types yes yes no yes yes
2+ train types (regional, inter-city) (2) no (4) no (3) no yes yes
Display user location on map or export bicycle track no yes no no no

(2): The GTFS Schedule specification has only Rail (covering intercity and long-distance travel) as route_type, see https://gtfs.org/schedule/reference/#routestxt. The Google Transit implementation of GTFS has extended route types differentiating between e.g. High Speed Rail Service and Suburban Railway (ICE and S-Bahn in Germany): https://developers.google.com/transit/gtfs/reference/extended-route-types

(3): There is only Rail, see TransportMode at https://github.com/HSLdevcom/digitransit-ui/blob/4f77be4bc7d5a9925e6055a19cf23da1cfd83bc3/app/constants.js#L18

(4): The OTP model.basic.TransitMode enum has only RAIL as value: https://github.com/opentripplanner/OpenTripPlanner/blob/c19bbabbc8f1e6ec4a2b03faa82d67a5a0dee6a7/src/main/java/org/opentripplanner/transit/model/basic/TransitMode.java#L10. The LegacyGraphQLApi is being developed by HSL (Digitransit). It has only RAIL as value of the Mode enum.

Open Source

  • MOTIS

  • Navitia

    • Multi-modal journeys computation
    • Transit data import from GTFS and NTFS
    • Routing options to combine bike and transit
      • https://doc.navitia.io/#journeys
      • Bike to reach and leave public transit (Journey request parameters first_section_mode[] and last_section_mode[]). User defined maximum bicycle duration (max_duration_to_pt).
      • User defined bike speed (bike_speed).
      • Specify vehicle types (Only bus and tram: allowed_id[]=physical_mode:Bus&allowed_id[]=physical_mode:Tramway)
  • RRRR (R4)

    • RRRR Rapid Real-time Routing
    • GTFS and GTFS-RT
    • RAPTOR
    • Last commit 2014
  • Mumoro

    • Multimodal routing: combining subway, walking and bike

Closed Source / Unknown