Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['14', '16']
node-version: ['16']
steps:
- name: checkout
uses: actions/checkout@v2
Expand Down Expand Up @@ -38,4 +38,5 @@ jobs:
with:
push: true
tags: |
derhuerst/generate-herrenberg-gtfs-flex:latest
derhuerst/generate-gtfs-flex:latest
derhuerst/generate-gtfs-flex:4
10 changes: 5 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
FROM node:alpine
LABEL org.opencontainers.image.title="generate-herrenberg-gtfs-flex"
LABEL org.opencontainers.image.description="Generate GTFS Flex for Herrenberg on-demand public transport service."
FROM node:lts-alpine
LABEL org.opencontainers.image.title="generate-gtfs-flex"
LABEL org.opencontainers.image.description="Given a GTFS Static feed, add GTFS Flex v2 to model on-demand public transport service."
LABEL org.opencontainers.image.authors="Jannis R <[email protected]>"
LABEL org.opencontainers.image.documentation="https://github.com/derhuerst/generate-herrenberg-gtfs-flex"
LABEL org.opencontainers.image.source="https://github.com/derhuerst/generate-herrenberg-gtfs-flex"
LABEL org.opencontainers.image.documentation="https://github.com/derhuerst/generate-gtfs-flex"
LABEL org.opencontainers.image.source="https://github.com/derhuerst/generate-gtfs-flex"
LABEL org.opencontainers.image.licenses="ISC"

WORKDIR /app
Expand Down
42 changes: 42 additions & 0 deletions bbnavi-flex-rules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict'

// These are the GTFS-Flex patching rules used by bbnavi (https://bbnavi.de).

const pickupTypes = require('gtfs-utils/pickup-types')
const dropOffTypes = require('gtfs-utils/drop-off-types')
const bookingTypes = require('gtfs-utils/booking-types')

const uvgLinienrufbusFlexSpec = {
id: 'uvg-linienrufbus',
pickup_type: pickupTypes.MUST_PHONE_AGENCY,
drop_off_type: dropOffTypes.MUST_COORDINATE_WITH_DRIVER,
bookingRule: {
booking_rule_id: 'uvg-linienrufbus',
booking_type: bookingTypes.SAME_DAY,
prior_notice_duration_min: 60,
message: `\
Anmeldung bis 60min vor Abfahrt, per Telefon (täglich von 08:00-24:00) oder online.`,
phone_number: '+49 3332 442 755',
// todo: separate flex specs for Angermünde & Gartz?
// https://uvg-online.com/rufbus-angermuende/
// https://uvg-online.com/rufbus-gartz/
info_url: 'https://uvg-online.com/rufbus/',
},
}
const uvgLinienrufbusRoutes = [
'459',
// todo: there are more
]
const uvgLinienrufbus = (origTrip, origRoute) => (
// todo: how do we distinguish Rufbus trips from regular trips?
// currently neither the DELFI GTFS nor the VBB GTFS provide a discerning field
uvgLinienrufbusRoutes.includes(origRoute.route_short_name)
? uvgLinienrufbusFlexSpec
: null
)

const bbnaviFlexRules = [
uvgLinienrufbus,
]

module.exports = bbnaviFlexRules
40 changes: 27 additions & 13 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,40 @@
set -e
set -o pipefail

if [ "$1" == '-h' ] || [ "$1" == '--help' ]; then
1>&2 cat << EOF
usage="$(
cat << EOF
usage:
docker run … [gtfs-directory]
docker run … <rules-file> [gtfs-directory]
example:
docker run -v \$PWD/data:/gtfs derhuerst/generate-gtfs-flex
docker run -v \$PWD/data:/data derhuerst/generate-gtfs-flex /data
docker run -v \$PWD/data:/gtfs derhuerst/generate-gtfs-flex stadtnavi-herrenberg-rules.js
docker run -v \$PWD/cfg:/cfg -v \$PWD/data:/data derhuerst/generate-gtfs-flex /cfg/rules.js /data
EOF
)"
if [ "$1" == '-h' ] || [ "$1" == '--help' ]; then
1>&2 echo "$usage"
exit 0
fi

if [ -n "$1" ]; then
1>&2 echo "running inside $1"
cd "$1"
pushd .
cd "$(dirname $0)"
rules_file="$1"
if [ -z "$rules_file" ]; then
1>&2 echo -e "missing/empty 1st argument: rules-file\n"
1>&2 echo "$usage"
exit 1
fi
rules_file="$(realpath "$1")"
popd

if [ -n "$2" ]; then
1>&2 echo "running inside $(realpath "$2")"
cd "$2"
fi

set -x

generate-booking-rules-txt /app/herrenberg-flex-rules.js *.txt | tee booking_rules.txt | wc -l
generate-locations-geojson /app/herrenberg-flex-rules.js *.txt | tee locations.geojson | wc -l
patch-routes-txt /app/herrenberg-flex-rules.js routes.txt | sponge routes.txt
patch-trips-txt /app/herrenberg-flex-rules.js *.txt | sponge trips.txt
patch-stop-times-txt /app/herrenberg-flex-rules.js *.txt | sponge stop_times.txt
generate-booking-rules-txt "$rules_file" *.txt | tee booking_rules.txt | wc -l
generate-locations-geojson "$rules_file" *.txt | tee locations.geojson | wc -l
patch-routes-txt "$rules_file" {routes,trips}.txt | sponge routes.txt
patch-trips-txt "$rules_file" *.txt | sponge trips.txt
patch-stop-times-txt "$rules_file" *.txt | sponge stop_times.txt
6 changes: 5 additions & 1 deletion gen-locations-geojson.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,13 @@ const readGtfsFile = createReadGtfsFile(requiredGtfsFiles, argv._.slice(1))
for (const [trip_id, spec] of byTripId) {
const {
id: specId,
radius,
stops,
} = spec

// skip flex specs without geographic buffer
if (!('radius' in spec)) continue
const radius = spec.radius

for (const s of stops) {
const locId = flexLocId(specId, s.stop_id)
if (printedLocs.has(locId)) continue
Expand Down
12 changes: 10 additions & 2 deletions lib/booking-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ const validateFlexSpec = require('./validate-flex-spec')
const computeAllBookingRules = async (flexRules, readGtfsFile) => {
const bookingRulesById = new Map() // booking_rule_id -> booking rule

for await (const r of readGtfsFile('routes')) {
const routes = new Map() // route_id -> route
for await (const r of await readGtfsFile('routes')) {
routes.set(r.route_id, r)
}

for await (const t of await readGtfsFile('trips')) {
if (!routes.has(t.route_id)) continue
const route = routes.get(t.route_id)

// find any matching GTFS-Flex rule
for (const flexRule of flexRules) {
const flexSpec = flexRule(r)
const flexSpec = flexRule(t, route)
if (flexSpec) {
validateFlexSpec(flexSpec)
const {bookingRule} = flexSpec
Expand Down
1 change: 0 additions & 1 deletion lib/flex-spec-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"type": "object",
"required": [
"id",
"radius",
"pickup_type",
"drop_off_type",
"bookingRule"
Expand Down
41 changes: 18 additions & 23 deletions lib/flex-specs-by-trip-id.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,51 @@
'use strict'


const validateFlexSpec = require('./validate-flex-spec')

const computeFlexSpecsByRouteId = async (flexRules, readGtfsFile) => {
const flexSpecsByRouteId = new Map() // route_id -> GTFS-Flex spec
const computeFlexSpecsByTripId = async (flexRules, readGtfsFile) => {
const routes = new Map() // route_id -> route
for await (const r of await readGtfsFile('routes')) {
routes.set(r.route_id, r)
}

const byTripId = new Map() // trip_id -> [flexSpec, route]
for await (const t of await readGtfsFile('trips')) {
if (!routes.has(t.route_id)) continue
const route = routes.get(t.route_id)

for await (const r of readGtfsFile('routes')) {
// find any matching GTFS-Flex rule
for (const flexRule of flexRules) {
const flexSpec = flexRule(r)
const flexSpec = flexRule(t, route)
if (flexSpec) {
validateFlexSpec(flexSpec)
flexSpecsByRouteId.set(r.route_id, flexSpec)
byTripId.set(t.trip_id, [flexSpec, route])
break
}
}
}

return flexSpecsByRouteId
}

const computeFlexSpecsByTripId = async (flexRules, readGtfsFile) => {
const byRouteId = await computeFlexSpecsByRouteId(flexRules, readGtfsFile)

const byTripId = new Map()
for await (const t of readGtfsFile('trips')) {
if (byRouteId.has(t.route_id)) {
const flexSpec = byRouteId.get(t.route_id)
byTripId.set(t.trip_id, flexSpec)
}
}

return byTripId
}

const computeFlexSpecsWithStopsByTripId = async (flexRules, readGtfsFile) => {
const byTripId = await computeFlexSpecsByTripId(flexRules, readGtfsFile)

const withStopsByTripId = new Map()
for (const [tripId, flexSpec] of byTripId.entries()) {
for (const [tripId, [flexSpec, route]] of byTripId.entries()) {
withStopsByTripId.set(tripId, {
...flexSpec,
route,
stops: new Set(),
})
}

const allStops = new Map()
for await (const s of readGtfsFile('stops')) {
for await (const s of await readGtfsFile('stops')) {
allStops.set(s.stop_id, s)
}

for await (const st of readGtfsFile('stop_times')) {
for await (const st of await readGtfsFile('stop_times')) {
if (!withStopsByTripId.has(st.trip_id)) continue
if (!allStops.has(st.stop_id)) continue

Expand All @@ -61,7 +57,6 @@ const computeFlexSpecsWithStopsByTripId = async (flexRules, readGtfsFile) => {
}

module.exports = {
computeFlexSpecsByRouteId,
computeFlexSpecsByTripId,
computeFlexSpecsWithStopsByTripId,
}
13 changes: 10 additions & 3 deletions lib/read-gtfs-files.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,21 @@ const createReadGtfsFile = (requiredFiles, gtfsFilePaths) => {
gtfsFiles[name] = path
}

const missingFileErr = name => new Error(`missing ${name}.txt file`)
const missingFileErr = (name) => {
const err = new Error(`missing ${name}.txt file`)
err.code = 'ENOENT'
err.notFound = true
err.statusCode === 404
return err
}

for (const name of requiredFiles) {
if (!gtfsFiles[name]) throw missingFileErr(name)
}

const readGtfsFile = (name) => {
const readGtfsFile = async (name) => {
if (!gtfsFiles[name]) throw missingFileErr(name)
return readCsv(gtfsFiles[name])
return await readCsv(gtfsFiles[name])
}
return readGtfsFile
}
Expand Down
2 changes: 2 additions & 0 deletions lib/validate-flex-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Haustürbedienung beim Absetzen 300m (Luftlinie) um die Haltestelle. Aufpreis 0,
},
}

// todo: add a bbnavi flex spec

doesNotThrow(() => {
validateFlexSpec(herrenbergCitybus)
})
Expand Down
Loading