├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── lib ├── departures.js ├── index.js ├── journeys.js └── stations.js ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{yml,yaml}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "indent": [ 5 | "error", 6 | "tab" 7 | ], 8 | "no-tabs": "off", 9 | "comma-dangle": [ 10 | "error", 11 | "always-multiline" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 16 14 | - 10 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # general 2 | .DS_Store 3 | *.log 4 | 5 | # node-specific 6 | node_modules 7 | package-lock.json 8 | yarn.lock 9 | shrinkwrap.yaml 10 | pnpm-lock.yaml 11 | dist 12 | .vscode 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /lib/departures.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { fetch } = require('fetch-ponyfill')() 4 | const isString = require('lodash/isString') 5 | const merge = require('lodash/merge') 6 | const moment = require('moment-timezone') 7 | const { stringify } = require('querystring') 8 | 9 | const defaults = {} 10 | 11 | const transformTime = (date, time) => { 12 | date = moment.tz(date, 'Europe/Berlin').subtract(4, 'hours') 13 | let depDate = moment.tz(date.format('DD.MM.YYYY') + ' ' + time, 'DD.MM.YYYY HH:mm', 'Europe/Berlin') 14 | // time before departure time? 15 | if (+depDate < +date) { 16 | depDate = depDate.add(1, 'days') 17 | } 18 | return depDate.toDate() 19 | } 20 | 21 | const transformDeparture = (date) => (d) => ({ 22 | line: { 23 | id: d.number, 24 | name: d.name, 25 | class: d.type, 26 | operator: d.operator, 27 | direction: d.direction, 28 | }, 29 | timetable: [ 30 | { 31 | departure: transformTime(date, d.time), 32 | departureDelay: ((+d.time_prognosis) || 0) * 60 * 1000, 33 | }, 34 | ...(d.later_departures || []).map((ld) => ({ 35 | departure: transformTime(date, ld.time), 36 | departureDelay: ((+ld.time_prognosis) || 0) * 60 * 1000, 37 | })), 38 | ], 39 | }) 40 | 41 | const departures = (station, date = Date.now(), opt = {}) => { 42 | // eslint-disable-next-line no-unused-vars 43 | const options = merge(defaults, opt) 44 | 45 | if (isString(station)) station = { id: station, type: 'station' } 46 | 47 | if (!isString(station.id)) throw new Error('invalid or missing station id') 48 | if (station.type !== 'station') throw new Error('invalid or missing station type') 49 | 50 | station = station.id 51 | 52 | const day = moment.tz(date, 'Europe/Berlin').format('DD.MM.YYYY') 53 | const time = moment.tz(date, 'Europe/Berlin').format('HH:mm') 54 | 55 | const body = { 56 | 'results[5][5][function]': 'ws_info_stop', 57 | 'results[5][5][data]': JSON.stringify([ 58 | { name: 'results[5][5][stop]', value: station }, 59 | { name: 'results[5][5][date]', value: day }, 60 | { name: 'results[5][5][time]', value: time }, 61 | { name: 'results[5][5][mode]', value: 'stop' }, 62 | ]), 63 | } 64 | 65 | return fetch('https://www.l.de/verkehrsbetriebe/fahrplan/abfahrten', { 66 | method: 'post', 67 | body: stringify(body), 68 | headers: { 69 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 70 | }, 71 | }) 72 | .then((res) => res.json()) 73 | .then((res) => res.connections.map(transformDeparture(date))) 74 | } 75 | 76 | module.exports = departures 77 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const stations = require('./stations') 4 | const journeys = require('./journeys') 5 | const departures = require('./departures') 6 | 7 | module.exports = { stations, journeys, departures } 8 | -------------------------------------------------------------------------------- /lib/journeys.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { fetch } = require('fetch-ponyfill')() 4 | const isString = require('lodash/isString') 5 | const toArray = require('lodash/toArray') 6 | const merge = require('lodash/merge') 7 | const moment = require('moment-timezone') 8 | const { stringify } = require('querystring') 9 | 10 | // todo: modes of transport, delays 11 | 12 | const defaults = { 13 | via: null, 14 | } 15 | 16 | const transformRouteStop = (s) => ({ 17 | type: 'station', 18 | id: s.id + '', 19 | name: s.name, 20 | coordinates: { 21 | longitude: s.lng, 22 | latitude: s.lat, 23 | }, 24 | // todo: arrival, departure, delay 25 | }) 26 | 27 | const transformLeg = (l) => ({ 28 | origin: { 29 | type: 'station', 30 | name: l.from.station, 31 | id: l.route[0].id + '', 32 | coordinates: { 33 | longitude: +l.route[0].lng, 34 | latitude: +l.route[0].lat, 35 | }, 36 | }, 37 | destination: { 38 | type: 'station', 39 | name: l.to.station, 40 | id: l.route[l.route.length - 1].id + '', 41 | coordinates: { 42 | longitude: +l.route[l.route.length - 1].lng, 43 | latitude: +l.route[l.route.length - 1].lat, 44 | }, 45 | }, 46 | line: { 47 | id: l.line, 48 | class: l.type, 49 | direction: l.direction, 50 | operator: l.operator, 51 | color: l.color, 52 | }, 53 | route: l.route.map(transformRouteStop), 54 | departure: moment.tz(l.from.datetime, 'YYYYMMDDHHmmss', 'Europe/Berlin').toDate(), 55 | departureDelay: (+l.from.shifting || 0) * 60 * 1000, // negative shifting? 56 | arrival: moment.tz(l.to.datetime, 'YYYYMMDDHHmmss', 'Europe/Berlin').toDate(), 57 | arrivalDelay: (+l.to.shifting || 0) * 60 * 1000, // negative shifting? 58 | departurePlatform: l.from.platform || null, 59 | arrivalPlatform: l.to.platform || null, 60 | }) 61 | 62 | const hashLeg = (l) => l.from.station + '@' + l.from.datetime + '@' + l.to.station + '@' + l.to.datetime + '@' + l.line + '@' + l.type 63 | const hashJourney = (j) => j.sections.map(hashLeg).join('-') 64 | 65 | const transformFare = (f) => ({ 66 | type: 'fare', 67 | model: f.name, 68 | amount: +(f.price) / 100, 69 | currency: 'EUR', 70 | }) 71 | 72 | const transformTariffs = (t) => (t && { 73 | model: t.tickets['1t0'].name, 74 | amount: +(t.tickets['1t0'].price) / 100, 75 | currency: 'EUR', 76 | fares: toArray(t.tickets).map(transformFare), 77 | }) 78 | 79 | const transformZones = (z) => (z && { 80 | departure: z.zone_start, 81 | arrival: z.zone_end, 82 | list: z.zones, // todo 83 | }) 84 | 85 | const transformJourney = (j) => (j && { 86 | type: 'journey', 87 | id: hashJourney(j), 88 | legs: j.sections.map(transformLeg), 89 | price: transformTariffs(j.tariffs), 90 | zones: transformZones(j.tariffs), 91 | }) 92 | 93 | const journeys = (origin, destination, date = Date.now(), opt = {}) => { 94 | const options = merge(defaults, opt) 95 | 96 | if (isString(origin)) origin = { id: origin, type: 'station' } 97 | if (isString(destination)) destination = { id: destination, type: 'station' } 98 | 99 | if (!isString(origin.id)) throw new Error('invalid or missing origin id') 100 | if (origin.type !== 'station') throw new Error('invalid or missing origin type') 101 | if (!isString(destination.id)) throw new Error('invalid or missing destination id') 102 | if (destination.type !== 'station') throw new Error('invalid or missing destination type') 103 | 104 | origin = origin.id 105 | destination = destination.id 106 | 107 | if (options.via) { 108 | if (isString(options.via)) options.via = { id: options.via, type: 'station' } 109 | if (!isString(options.via.id)) throw new Error('invalid or missing options.via id') 110 | if (options.via.type !== 'station') throw new Error('invalid or missing options.via type') 111 | options.via = options.via.id 112 | } 113 | 114 | const day = moment.tz(date, 'Europe/Berlin').format('DD.MM.YYYY') 115 | const time = moment.tz(date, 'Europe/Berlin').format('HH:mm') 116 | 117 | const body = { 118 | 'results[5][5][function]': 'ws_find_connections', 119 | 'results[5][5][data]': JSON.stringify([ 120 | { name: 'results[5][5][is_extended]', value: '1' }, 121 | { name: 'results[5][5][from]', value: origin }, 122 | { name: 'results[5][5][to]', value: destination }, 123 | { name: 'results[5][5][via]', value: options.via || '' }, 124 | { name: 'results[5][5][time_mode]', value: 'departure' }, 125 | { name: 'results[5][5][time]', value: time }, 126 | { name: 'results[5][5][date]', value: day }, 127 | { name: 'results[5][5][means_of_transport][]', value: 'STR' }, 128 | { name: 'results[5][5][means_of_transport][]', value: 'BUS' }, 129 | { name: 'results[5][5][means_of_transport][]', value: 'S/U' }, 130 | { name: 'results[5][5][means_of_transport][]', value: 'RB/RE' }, 131 | { name: 'results[5][5][mode]', value: 'connection' }, 132 | ]), 133 | } 134 | 135 | return fetch('https://www.l.de/verkehrsbetriebe/fahrplan/verbindung', { 136 | method: 'post', 137 | body: stringify(body), 138 | headers: { 139 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 140 | }, 141 | }) 142 | .then((res) => res.json()) 143 | .then((res) => toArray(res.connections)) 144 | .then((res) => res.map(transformJourney)) 145 | } 146 | 147 | module.exports = journeys 148 | -------------------------------------------------------------------------------- /lib/stations.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { fetch } = require('fetch-ponyfill')() 4 | const isString = require('lodash/isString') 5 | const merge = require('lodash/merge') 6 | const { stringify, escape } = require('querystring') 7 | const moment = require('moment-timezone') 8 | const Queue = require('p-queue').default 9 | 10 | // todo: POI 11 | const defaults = { 12 | limit: 5, 13 | } 14 | 15 | // todo: umlauts 16 | 17 | // this is necessary because of an unlucky situation with IDs in the data 18 | const getStationID = (s) => { 19 | const body = { 20 | 'results[5][5][function]': 'ws_info_stop', 21 | 'results[5][5][data]': JSON.stringify([ 22 | { name: 'results[5][5][stop]', value: s }, 23 | { name: 'results[5][5][date]', value: moment.tz('Europe/Berlin').add(1, 'days').format('DD.MM.YYYY') }, 24 | { name: 'results[5][5][time]', value: '' }, 25 | { name: 'results[5][5][mode]', value: 'stop' }, 26 | ]), 27 | } 28 | 29 | return fetch('https://www.l.de/verkehrsbetriebe/fahrplan/abfahrten', { 30 | method: 'post', 31 | body: stringify(body), 32 | headers: { 33 | 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', 34 | }, 35 | }) 36 | .then((res) => res.json()) 37 | .then((res) => { 38 | if (res.station_info.name !== s) { 39 | throw new Error(`internal error: valid station ID! Expected "${s}", got "${res.station_info.name}"`) 40 | } 41 | return res.station_info.id 42 | }) 43 | // todo: what about res.lines (empty at the moment) and res.epon_ids? 44 | } 45 | 46 | const addStationID = (s) => 47 | getStationID(s.name) 48 | .then((id) => merge({ id: id + '' }, s)) 49 | 50 | const transformStation = (s) => ({ 51 | type: 'station', 52 | name: s.name, 53 | coordinates: { 54 | longitude: +s.lng, 55 | latitude: +s.lat, 56 | }, 57 | }) 58 | 59 | const stations = (query, opt = {}) => { 60 | if (!query || !isString(query)) { 61 | throw new Error('query must be a string != ""') 62 | } 63 | 64 | const q = new Queue({ concurrency: 4 }) 65 | 66 | const options = merge(defaults, opt) 67 | return fetch(`https://www.l.de/ajax_de?mode=autocomplete&q=${escape(query)}&poi=&limit=${options.limit}`) 68 | .then((res) => res.json()) 69 | .then((res) => res.stations) 70 | .then((res) => res.map(transformStation)) 71 | .then((res) => q.addAll(res.map((s) => () => addStationID(s)))) 72 | .then((res) => res.filter((x) => !!x.id)) 73 | } 74 | 75 | module.exports = stations 76 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, Julius Tens 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lvb", 3 | "version": "1.0.6", 4 | "description": "Inofficial LVB (Leipziger Verkehrsbetriebe) API client.", 5 | "keywords": [ 6 | "deutschland", 7 | "germany", 8 | "leipzig", 9 | "lvb", 10 | "public", 11 | "sachsen", 12 | "saxony", 13 | "transport", 14 | "öpnv" 15 | ], 16 | "homepage": "https://github.com/juliuste/lvb", 17 | "bugs": "https://github.com/juliuste/lvb/issues", 18 | "repository": "juliuste/lvb", 19 | "license": "ISC", 20 | "author": "Julius Tens ", 21 | "contributors": [ 22 | "Justin Kromlinger ", 23 | "Jörg Reichert (http://joergreichert.github.io/)" 24 | ], 25 | "main": "lib/index.js", 26 | "files": [ 27 | "lib/*" 28 | ], 29 | "scripts": { 30 | "check-deps": "depcheck", 31 | "fix": "npm run lint -- --fix", 32 | "lint": "eslint lib test.js", 33 | "prepublishOnly": "npm test", 34 | "test": "npm run lint && npm run check-deps && node test" 35 | }, 36 | "dependencies": { 37 | "fetch-ponyfill": "^6.1.1", 38 | "lodash": "^4.17.21", 39 | "moment-timezone": "^0.5.33", 40 | "p-queue": "^6.6.2" 41 | }, 42 | "devDependencies": { 43 | "depcheck": "^1.4.2", 44 | "eslint": "^7.32.0", 45 | "eslint-config-standard": "^16.0.3", 46 | "eslint-plugin-import": "^2.25.2", 47 | "eslint-plugin-node": "^11.1.0", 48 | "eslint-plugin-promise": "^5.1.1", 49 | "tape": "^5.3.1", 50 | "tape-promise": "^4.0.0" 51 | }, 52 | "engines": { 53 | "node": ">=10" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # lvb 2 | 3 | Client for the [LVB](https://l.de/verkehrsbetriebe) (Leipziger Verkehrsbetriebe) API. Inofficial, please ask LVB for permission before using this module in production. **Actually, there should be no need for projects like this since municipal public transportation endpoints should be open to the public. It's 2021.** 4 | 5 | [![npm version](https://img.shields.io/npm/v/lvb.svg)](https://www.npmjs.com/package/lvb) 6 | [![license](https://img.shields.io/github/license/juliuste/lvb.svg?style=flat)](license) 7 | 8 | ## Installation 9 | 10 | ```shell 11 | npm install --save lvb 12 | ``` 13 | 14 | ## Usage 15 | 16 | This package mostly returns data in the [*Friendly Public Transport Format*](https://github.com/public-transport/friendly-public-transport-format): 17 | 18 | - [`stations(query, [opt])`](#stationsquery-opt) - Search for stations 19 | - [`departures(station, date = Date.now())`](#departuresstation-date--datenow) - Departures at a given station 20 | - [`journeys(origin, destination, date = Date.now(), [opt])`](#journeysorigin-destination-date--datenow-opt) - Search for journeys between stations 21 | 22 | ### `stations(query, [opt])` 23 | 24 | Using `lvb.stations`, you can query stations operated bei LVB. 25 | 26 | ```js 27 | const stations = require('lvb').stations 28 | 29 | stations('Nationalbibliothek').then(console.log) 30 | ``` 31 | 32 | Returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/promise) that will resolve in an array of `station`s in the [*Friendly Public Transport Format*](https://github.com/public-transport/friendly-public-transport-format) which looks as follows: 33 | 34 | ```js 35 | 36 | [ 37 | { 38 | "id": "11558", 39 | "type": "station", 40 | "name": "Leipzig, Deutsche Nationalbibliothek", 41 | "coordinates": { 42 | "longitude": 12.396131411662, 43 | "latitude": 51.323542325868 44 | } 45 | } 46 | // … 47 | ] 48 | ``` 49 | 50 | `defaults`, partially overridden by the `opt` parameter, looks as follows: 51 | 52 | ```js 53 | const defaults = { 54 | limit: 5 // Maximum number of returned results. CAUTION: Because of something unlucky that happens to station ids in the API, a `stations` request will spawn (number of results + 1) requests. Keep this in mind when increasing this threshold. 55 | } 56 | ``` 57 | 58 | ### `departures(station, date = Date.now())` 59 | 60 | Using `lvb.departures`, you can get departures at a given station for a specific date and time. 61 | 62 | ```js 63 | const departures = require('lvb').departures 64 | 65 | const Nationalbibliothek = '11558' 66 | 67 | departures(Nationalbibliothek, new Date()) 68 | ``` 69 | 70 | Returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/promise) that will resolve in a list of objects (one object per direction per line) like this: 71 | 72 | ```js 73 | [ 74 | { 75 | "line": { 76 | "id": "16", 77 | "name": "Str 16", // yeah, that really looks like this :/ 78 | "class": "StN", 79 | "operator": "LVB", 80 | "direction": "Lößnig über Connewitz, Kreuz" 81 | }, 82 | "timetable": [ 83 | { 84 | "departure": "2017-10-09T16:09:00.000Z", // JS Date() object 85 | "departureDelay": 0 86 | }, 87 | { 88 | "departure": "2017-10-09T16:19:00.000Z", // JS Date() object 89 | "departureDelay": 0 90 | }, 91 | { 92 | "departure": "2017-10-09T16:29:00.000Z", // JS Date() object 93 | "departureDelay": 0 94 | }, 95 | { 96 | "departure": "2017-10-09T16:39:00.000Z", // JS Date() object 97 | "departureDelay": 0 98 | }, 99 | { 100 | "departure": "2017-10-09T16:51:00.000Z", // JS Date() object 101 | "departureDelay": 0 102 | } 103 | ] 104 | } 105 | // … 106 | ] 107 | ``` 108 | 109 | ### `journeys(origin, destination, date = Date.now(), [opt])` 110 | 111 | Using `lvb.journeys`, you can get directions and prices for routes from A to B. 112 | 113 | ```js 114 | const journeys = require('lvb').journeys 115 | 116 | journeys(origin, destination, date = Date.now(), opt = defaults) 117 | 118 | const Nationalbibliothek = '11558' 119 | const Messe = '10818' 120 | const date = new Date() 121 | 122 | journeys(Nationalbibliothek, Messe, date) 123 | .then(console.log) 124 | .catch(console.error) 125 | ``` 126 | 127 | Returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/promise) that will resolve with an array of `journey`s in the [*Friendly Public Transport Format*](https://github.com/public-transport/friendly-public-transport-format) which looks as follows. 128 | *Note that the legs are not (fully) spec-compatible, as the `schedule` is missing (see the `line` and `route` keys instead).* 129 | 130 | ```js 131 | [ 132 | { 133 | "type": "journey", 134 | "id": "Leipzig, Deutsche Nationalbibliothek@20171009173100@Leipzig, Wilhelm-Leuschner-Platz@20171009173900@SEV16@BUN-Leipzig, Wilhelm-Leuschner-Platz@20171009174200@Leipzig, Messegelände@20171009180800@16@STN", 135 | "legs": [ 136 | { 137 | "origin": { 138 | "type": "station", 139 | "name": "Leipzig, Deutsche Nationalbibliothek", 140 | "id": 11558, 141 | "coordinates": { 142 | "longitude": 12.395702, 143 | "latitude": 51.32357 144 | } 145 | }, 146 | "destination": { 147 | "type": "station", 148 | "name": "Leipzig, Wilhelm-Leuschner-Platz", 149 | "id": 12992, 150 | "coordinates": { 151 | "longitude": 12.375872, 152 | "latitude": 51.335876 153 | } 154 | }, 155 | "line": { 156 | "id": "SEV16", 157 | "class": "BUN", 158 | "direction": "Wilhelm-Leuschner-Platz", 159 | "operator": "Leipziger Verkehrsbetriebe", 160 | "color": "#017C46" 161 | }, 162 | "route": [ 163 | { 164 | "type": "station", 165 | "id": 11558, 166 | "name": "Leipzig, Deutsche Nationalbibliothek", 167 | "coordinates": { 168 | "longitude": 12.395702, 169 | "latitude": 51.32357 170 | } 171 | }, 172 | { 173 | "type": "station", 174 | "id": 11557, 175 | "name": "Leipzig, Johannisallee", 176 | "coordinates": { 177 | "longitude": 12.388807, 178 | "latitude": 51.327309 179 | } 180 | } 181 | // … 182 | ], 183 | "departure": "2017-10-09T15:31:00.000Z", // JS Date() object 184 | "departureDelay": 0, 185 | "arrival": "2017-10-09T15:39:00.000Z", // JS Date() object 186 | "arrivalDelay": 0, 187 | "departurePlatform": null, 188 | "arrivalPlatform": null 189 | }, 190 | { 191 | "origin": { 192 | "type": "station", 193 | "name": "Leipzig, Wilhelm-Leuschner-Platz", 194 | "id": 12992, 195 | "coordinates": { 196 | "longitude": 12.375872, 197 | "latitude": 51.335876 198 | } 199 | }, 200 | "destination": { 201 | "type": "station", 202 | "name": "Leipzig, Messegelände", 203 | "id": 10818, 204 | "coordinates": { 205 | "longitude": 12.396583, 206 | "latitude": 51.396724 207 | } 208 | }, 209 | "line": { 210 | "id": "16", 211 | "class": "STN", 212 | "direction": "Messegelände", 213 | "operator": "Leipziger Verkehrsbetriebe", 214 | "color": "#017C46" 215 | }, 216 | "route": [ 217 | { 218 | "type": "station", 219 | "id": 12992, 220 | "name": "Leipzig, Wilhelm-Leuschner-Platz", 221 | "coordinates": { 222 | "longitude": 12.375872, 223 | "latitude": 51.335876 224 | } 225 | }, 226 | { 227 | "type": "station", 228 | "id": 13002, 229 | "name": "Leipzig, Augustusplatz", 230 | "coordinates": { 231 | "longitude": 12.382012, 232 | "latitude": 51.338905 233 | } 234 | } 235 | // … 236 | ], 237 | "departure": "2017-10-09T15:42:00.000Z", // JS Date() object 238 | "departureDelay": 0, 239 | "arrival": "2017-10-09T16:08:00.000Z", // JS Date() object 240 | "arrivalDelay": 0, 241 | "departurePlatform": null, 242 | "arrivalPlatform": null 243 | } 244 | ], 245 | "price": { 246 | "model": "Einzelfahrkarte", 247 | "amount": 2.6, 248 | "currency": "EUR", 249 | "fares": [ 250 | { 251 | "type": "fare", 252 | "model": "Einzelfahrkarte", 253 | "amount": 2.6, 254 | "currency": "EUR" 255 | }, 256 | { 257 | "type": "fare", 258 | "model": "Einzelfahrkarte Kind", 259 | "amount": 1.2, 260 | "currency": "EUR" 261 | }, 262 | { 263 | "type": "fare", 264 | "model": "4-Fahrten-Karte", 265 | "amount": 10.4, 266 | "currency": "EUR" 267 | } 268 | // … 269 | ] 270 | }, 271 | "zones": { 272 | "departure": "110", 273 | "arrival": "110", 274 | "list": "110" 275 | } 276 | } 277 | // … 278 | ] 279 | ``` 280 | 281 | ---- 282 | 283 | `defaults`, partially overridden by the `opt` parameter, looks like this: 284 | 285 | ```js 286 | const defaults = { 287 | via: null // station id 288 | } 289 | ``` 290 | 291 | ## See also 292 | 293 | - [FPTF](https://github.com/public-transport/friendly-public-transport-format) - "Friendly public transport format" 294 | - [FPTF-modules](https://github.com/public-transport/friendly-public-transport-format/blob/master/modules.md) - modules that also use FPTF 295 | 296 | ## Contributing 297 | 298 | If you found a bug or want to propose a feature, feel free to visit [the issues page](https://github.com/juliuste/lvb/issues). 299 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tapeWithoutPromise = require('tape') 4 | const addPromiseSupport = require('tape-promise').default 5 | const tape = addPromiseSupport(tapeWithoutPromise) 6 | const isObject = require('lodash/isObject') 7 | const isNumber = require('lodash/isNumber') 8 | const isString = require('lodash/isString') 9 | const isDate = require('lodash/isDate') 10 | const lvb = require('.') 11 | 12 | tape('lvb.stations', async t => { 13 | const stations = await lvb.stations('Nationalbibliothek') 14 | t.ok(stations.length > 0, 'length') 15 | const [dnb] = stations 16 | t.ok(dnb.type === 'station', 'dnb type') 17 | t.ok(dnb.id === '11558', 'dnb id') 18 | t.ok(dnb.name === 'Leipzig, Deutsche Nationalbibliothek', 'dnb name') 19 | t.ok(isObject(dnb.coordinates) && isNumber(dnb.coordinates.latitude) && isNumber(dnb.coordinates.longitude), 'stations dnb coordinates') 20 | t.end() 21 | }) 22 | 23 | const isDNB = (s) => 24 | s.name === 'Leipzig, Deutsche Nationalbibliothek' && 25 | s.id === '11558' 26 | 27 | const isMesse = (s) => 28 | s.name === 'Leipzig, Messegelände' && 29 | s.id === '10818' 30 | 31 | const isStation = (s) => 32 | s.type === 'station' && 33 | isObject(s.coordinates) && 34 | s.coordinates.longitude > 0 && 35 | s.coordinates.latitude > 0 36 | 37 | const isLine = (l) => 38 | isString(l.id) && l.id.length > 0 && 39 | isString(l.class) && l.class.length > 0 && 40 | isString(l.direction) && l.direction.length > 0 && 41 | isString(l.operator) && 42 | isString(l.color) && l.color.length === 7 43 | 44 | const isFare = (f) => 45 | f.type === 'fare' && 46 | isString(f.model) && f.model.length > 0 && 47 | isNumber(f.amount) && f.amount > 0 && 48 | f.currency === 'EUR' 49 | 50 | tape('lvb.journeys', async t => { 51 | // DNB to Messegelände 52 | const journeys = await lvb.journeys('11558', '10818') 53 | t.ok(journeys.length > 0, 'length') 54 | const [journey] = journeys 55 | t.ok(journey.type === 'journey', 'journey type') 56 | t.ok(journey.id, 'journey id') 57 | 58 | t.ok(isDNB(journey.legs[0].origin), 'journey origin') 59 | t.ok(isMesse(journey.legs[journey.legs.length - 1].destination), 'journey destination') 60 | 61 | const leg = journey.legs[0] 62 | t.ok(isStation(leg.origin), 'journey leg origin') 63 | t.ok(isStation(leg.destination), 'journey leg destination') 64 | t.ok(isDate(leg.departure), 'journey leg departure') 65 | t.ok(isNumber(leg.departureDelay), 'journey leg departureDelay') 66 | t.ok(isDate(leg.arrival), 'journey leg arrival') 67 | t.ok(isNumber(leg.arrivalDelay), 'journey leg arrivalDelay') 68 | t.ok(isLine(leg.line), 'journey leg line') 69 | t.ok(leg.route.length >= 2, 'journey leg route length') 70 | t.ok(leg.route.every(isStation), 'journey leg route') 71 | t.ok(leg.route[0].id === leg.origin.id, 'journey leg route:first id') 72 | t.ok(leg.route[leg.route.length - 1].id === leg.destination.id, 'journey leg route:last id') 73 | 74 | t.ok(journey.price.currency === 'EUR', 'journey price currency') 75 | t.ok(journey.price.amount >= 2, 'journey price amount') 76 | t.ok(journey.price.model === 'Einzelfahrkarte', 'journey price model') 77 | t.ok(journey.price.fares.length > 0, 'journey price fares length') 78 | t.ok(journey.price.fares.every(isFare), 'journey price fares') 79 | t.end() 80 | }) 81 | 82 | const isDepLine = (l) => 83 | isString(l.id) && l.id.length > 0 && 84 | isString(l.class) && l.class.length > 0 && 85 | isString(l.direction) && l.direction.length > 0 && 86 | isString(l.operator) && 87 | isString(l.name) && l.name.length > 0 88 | 89 | tape('lvb.departures', async t => { 90 | const departures = await lvb.departures('10818') 91 | t.ok(departures.length > 0, 'length') 92 | const [departuresForLine] = departures 93 | t.ok(isDepLine(departuresForLine.line), 'departure line') 94 | t.ok(departuresForLine.timetable.length > 0, 'departure timetable length') 95 | t.ok(isDate(departuresForLine.timetable[0].departure), 'departure timetable:first departure') 96 | t.ok(isNumber(departuresForLine.timetable[0].departureDelay), 'departure timetable:first departureDelay') 97 | t.end() 98 | }) 99 | --------------------------------------------------------------------------------