├── .gitignore
├── test
├── config.example.js
├── browser
│ └── index.html
├── data
│ ├── station-008011160.json
│ ├── departure-moskva.json
│ ├── station-berlin.json
│ ├── itinerary-ic142.json
│ ├── arrivals-berlin.json
│ └── departures-berlin.json
├── lib
│ ├── querystring.js
│ ├── date-util.js
│ ├── request.js
│ └── parsers.js
└── index.js
├── .editorconfig
├── lib
├── request
│ ├── index.js
│ ├── browser.js
│ └── node.js
├── querystring.js
├── date-util.js
└── parsers.js
├── webpack
├── config.min.js
├── config.js
└── test.js
├── .eslintrc
├── LICENSE.md
├── package.json
├── index.js
├── dist
├── fahrplan.min.js
└── fahrplan.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | test/config.js
3 | test/browser/test.js
4 |
--------------------------------------------------------------------------------
/test/config.example.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | // Add your API key here
5 | key: 'topsecret',
6 | };
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | # I normally use tabs, but this project accidentally started with spaces, so spaces it is.
3 | indent_style = space
4 | indent_size = 2
5 |
6 |
--------------------------------------------------------------------------------
/lib/request/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | if (typeof window !== 'undefined') module.exports = require('./browser');
4 | else module.exports = require('./node');
5 |
--------------------------------------------------------------------------------
/test/browser/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Fahrplan.js
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/webpack/config.min.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var webpack = require('webpack');
3 |
4 | var config = require('./config');
5 | config.output.filename = 'fahrplan.min.js';
6 | config.plugins.push(new webpack.optimize.UglifyJsPlugin());
7 | module.exports = config;
8 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint:recommended",
3 | "rules": {
4 | "comma-dangle": [2, "always-multiline"]
5 | },
6 | "env": {
7 | "es6": false,
8 | "browser": true,
9 | "node": true,
10 | "mocha": true
11 | },
12 | "plugins": []
13 | }
14 |
--------------------------------------------------------------------------------
/test/data/station-008011160.json:
--------------------------------------------------------------------------------
1 | {
2 | "LocationList":{
3 | "noNamespaceSchemaLocation":"http://open-api.bahn.de/bin/rest.exe/v1.0/xsd?name=hafasRestLocation.xsd",
4 | "StopLocation":{
5 | "name":"Berlin Hbf",
6 | "lon":"13.369548",
7 | "lat":"52.525589",
8 | "id":"008011160"
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/webpack/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var webpack = require('webpack');
3 | var pr = require('path').resolve;
4 |
5 | module.exports = {
6 | entry: '.',
7 | output: {
8 | library: 'Fahrplan',
9 | libraryTarget: 'umd',
10 | path: pr(__dirname, '../dist/'),
11 | filename: 'fahrplan.js',
12 | },
13 | node: {
14 | http: false,
15 | https: false,
16 | },
17 | externals: {
18 | 'es6-promise': true,
19 | },
20 | plugins: [],
21 | };
22 |
--------------------------------------------------------------------------------
/lib/request/browser.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /* global Promise */
4 |
5 | module.exports = function request(url) {
6 | return new Promise(function (resolve, reject) {
7 | var req = new XMLHttpRequest();
8 | var protocol = url.split(':').shift().toLowerCase();
9 | if (protocol !== 'https' && protocol !== 'http') {
10 | throw new Error('Unsupported protocol (' + protocol + ')');
11 | }
12 |
13 | req.open('GET', url, true);
14 |
15 | req.onload = function () {
16 | resolve({ data: req.responseText });
17 | }
18 |
19 | req.onerror = reject;
20 |
21 | req.send();
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/webpack/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var webpack = require('webpack');
3 | var pr = require('path').resolve;
4 |
5 | var definePlugin = new webpack.DefinePlugin({
6 | BROWSER: true,
7 | });
8 |
9 | module.exports = {
10 | entry: [
11 | // Most of the library modules are already tested in node.js. Only test
12 | // the actual API and modules with browser-specific code
13 | 'mocha!./test/index.js', // API
14 | 'mocha!./test/lib/request.js', // is browser-specific
15 | ],
16 | output: {
17 | path: pr(__dirname, '../test/browser/'),
18 | filename: 'test.js',
19 | },
20 |
21 | plugins: [definePlugin],
22 | };
23 |
--------------------------------------------------------------------------------
/test/data/departure-moskva.json:
--------------------------------------------------------------------------------
1 | {
2 | "DepartureBoard":{
3 | "noNamespaceSchemaLocation":"http://open-api.bahn.de/bin/rest.exe/v1.0/xsd?name=hafasRestDepartureBoard.xsd",
4 | "Departure":{
5 | "name":"EN 23",
6 | "type":"EN",
7 | "stopid":"2000058",
8 | "stop":"Moskva Belorusskaja",
9 | "time":"22:15",
10 | "date":"2016-02-26",
11 | "direction":"Strasbourg",
12 | "JourneyDetailRef":{
13 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=844797%2F286761%2F867826%2F152314%2F80%3Fdate%3D2016-02-26%26station_evaId%3D2000058%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
14 | }
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/lib/request/node.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Promise = require('es6-promise').Promise;
4 | var https = require('https');
5 | var http = require('http');
6 |
7 | module.exports = function request(url) {
8 | return new Promise(function (resolve, reject) {
9 | var protocol = url.toLowerCase().split(':').shift();
10 | var get;
11 | if (protocol === 'https') get = https.get;
12 | else if (protocol === 'http') get = http.get;
13 | else throw new Error('Unsupported protocol (' + protocol + ')');
14 |
15 | var req = get(url, function (res) {
16 | var data = '';
17 | res.setEncoding('utf8');
18 | res.on('data', function (chunk) { data += chunk; });
19 | res.on('end', function () {
20 | resolve({ data: data });
21 | });
22 | });
23 |
24 | req.on('error', reject);
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/lib/querystring.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Minimalistic re-implementation of querystring.stringify
4 |
5 | module.exports = {
6 | stringify: function (params, separator, equals) {
7 | if (!separator) separator = '&';
8 | if (!equals) equals = '=';
9 |
10 | var output = [];
11 |
12 | function serialize(key, value) {
13 | return encodeURIComponent(key) + equals + encodeURIComponent(value);
14 | }
15 |
16 | var keys = Object.keys(params);
17 | keys.forEach(function (key) {
18 | var value = params[key];
19 |
20 | if (Array.isArray(value)) {
21 | value.forEach(function (arrayValue) {
22 | output.push(serialize(key, arrayValue));
23 | });
24 | } else {
25 | output.push(serialize(key, value));
26 | }
27 | });
28 |
29 | return output.join(separator);
30 | },
31 | }
32 |
--------------------------------------------------------------------------------
/lib/date-util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function zeroPad(number, length) {
4 | if (length === undefined) length = 2;
5 | number = '' + number;
6 | while (number.length < length) number = '0' + number;
7 | return number;
8 | }
9 |
10 | module.exports = {
11 | formatDate: function (input) {
12 | input = new Date(input);
13 | var dd = zeroPad(input.getDate());
14 | var mm = zeroPad(input.getMonth() + 1);
15 | var yyyy = zeroPad(input.getFullYear(), 4);
16 | return [ yyyy, mm, dd ].join('-');
17 | },
18 |
19 | formatTime: function (input) {
20 | input = new Date(input);
21 | var hh = zeroPad(input.getHours());
22 | var mm = zeroPad(input.getMinutes());
23 | return hh + ':' + mm;
24 | },
25 |
26 | parse: function (date, time) {
27 | date = date.split('-');
28 | time = time.split(':');
29 |
30 | var year = parseInt(date[0], 10);
31 | var month = parseInt(date[1], 10) - 1;
32 | var day = parseInt(date[2], 10);
33 |
34 | var hour = parseInt(time[0], 10);
35 | var minute = parseInt(time[1], 10);
36 |
37 | return new Date(year, month, day, hour, minute);
38 | },
39 | }
40 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2016 [Philipp Bock](http://philippbock.de/)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | **The Software is provided "as is", without warranty of any kind, express or
14 | implied, including but not limited to the warranties of merchantability,
15 | fitness for a particular purpose and noninfringement. In no event shall the
16 | authors or copyright holders be liable for any claim, damages or other
17 | liability, whether in an action of contract, tort or otherwise, arising from,
18 | out of or in connection with the Software or the use or other dealings in the
19 | Software.**
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fahrplan",
3 | "version": "0.3.4",
4 | "description": "A client for Deutsche Bahn's open timetable API",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha test/index.js",
8 | "build": "webpack --config webpack/config.js && webpack --config webpack/config.min.js",
9 | "test:browser": "webpack --config webpack/test.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/pbock/fahrplan.git"
14 | },
15 | "keywords": [
16 | "train",
17 | "bahn",
18 | "station",
19 | "db",
20 | "schedule",
21 | "timetable",
22 | "api"
23 | ],
24 | "author": "Philipp Bock (http://philippbock.de/)",
25 | "license": "MIT",
26 | "bugs": {
27 | "url": "https://github.com/pbock/fahrplan/issues"
28 | },
29 | "homepage": "https://github.com/pbock/fahrplan#readme",
30 | "devDependencies": {
31 | "chai": "^3.5.0",
32 | "mocha": "^2.4.5",
33 | "mocha-loader": "^0.7.1",
34 | "moment": "^2.11.2",
35 | "webpack": "^1.12.14"
36 | },
37 | "dependencies": {
38 | "es6-promise": "^3.1.2"
39 | },
40 | "browser": "./dist/fahrplan.js"
41 | }
42 |
--------------------------------------------------------------------------------
/test/lib/querystring.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var expect = require('chai').expect;
4 |
5 | var qs = require('../../lib/querystring');
6 | var nodeQs = require('querystring');
7 |
8 | describe('querystring', function () {
9 | describe('#stringify()', function () {
10 | var stringify = qs.stringify;
11 |
12 | it('converts { foo: "bar", baz: "quux" } to foo=bar&baz=quux', function () {
13 | expect(stringify({ foo: 'bar', baz: 'quux' })).to.equal('foo=bar&baz=quux');
14 | });
15 |
16 | it('converts numbers to strings', function () {
17 | expect(stringify({ number: 1 })).to.equal('number=1');
18 | });
19 |
20 | it('repeats the key for arrays', function () {
21 | expect(stringify({ array: [ 'foo', 'bar', 42 ] })).to.equal('array=foo&array=bar&array=42');
22 | });
23 |
24 | it('can be passed a separator as its second argument', function () {
25 | expect(stringify({ foo: 'bar', baz: 'quux' }, ';')).to.equal('foo=bar;baz=quux');
26 | });
27 |
28 | it('can be passed an equals sign as its third argument', function () {
29 | expect(stringify({ foo: 'bar', baz: 'quux' }, null, ':')).to.equal('foo:bar&baz:quux');
30 | });
31 |
32 | it('encodes URI components', function () {
33 | expect(stringify({ "föø": 'bär båz _~!* 🍪' })).to.equal('f%C3%B6%C3%B8=b%C3%A4r%20b%C3%A5z%20_~!*%20%F0%9F%8D%AA');
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/lib/date-util.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var expect = require('chai').expect;
4 | var moment = require('moment');
5 |
6 | var dateUtil = require('../../lib/date-util');
7 |
8 | describe('dateUtil', function () {
9 | describe('#formatDate', function () {
10 | var fd = dateUtil.formatDate;
11 | it('converts a Date object to a string in YYYY-MM-DD notation', function () {
12 | expect(fd(new Date())).to.be.a('string');
13 | expect(fd(new Date(2015, 0, 1))).to.equal('2015-01-01');
14 | expect(fd(new Date(2016, 11, 24))).to.equal('2016-12-24');
15 | });
16 |
17 | it('works with timestamps too', function () {
18 | var timestamp = new Date(1850, 5, 21).valueOf();
19 | expect(fd(timestamp)).to.equal('1850-06-21');
20 | });
21 |
22 | it('works with moment objects too', function () {
23 | expect(fd(moment({ year: 2020, month: 3, day: 15 }))).to.equal('2020-04-15');
24 | });
25 | });
26 |
27 | describe('#formatTime', function () {
28 | var ft = dateUtil.formatTime;
29 | it('converts a Date object to a string in HH:MM notation', function () {
30 | expect(ft(new Date(2016, 1, 25, 18, 0))).to.equal('18:00');
31 | expect(ft(new Date(2017, 0, 1))).to.equal('00:00');
32 | });
33 |
34 | it('works with timestamps too', function () {
35 | var timestamp = new Date(2017, 0, 1, 13, 37).valueOf();
36 | expect(ft(timestamp)).to.equal('13:37');
37 | });
38 |
39 | it('works with moment objects too', function () {
40 | expect(ft(moment({ hour: 23, minute: 42 }))).to.equal('23:42');
41 | });
42 | });
43 |
44 | describe('#parse', function () {
45 | var p = dateUtil.parse;
46 | it('converts its arguments from (YYYY-MM-DD, HH:MM) notation to a Date object in the local timezone', function () {
47 | expect(p('2016-01-01', '00:00')).to.deep.equal(new Date(2016, 0, 1));
48 | expect(p('1900-12-31', '23:59')).to.deep.equal(new Date(1900, 11, 31, 23, 59));
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/test/lib/request.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var expect = require('chai').expect;
4 |
5 | var request = require('../../lib/request');
6 |
7 | describe('request', function () {
8 | it('is a function', function () {
9 | expect(request).to.be.a('Function');
10 | });
11 |
12 | it('returns a Promise', function () {
13 | var returnValue = request('http://example.com/');
14 | // No point testing for a specific prototype (because polyfill may not be
15 | // necessary everywhere and is not included in the browser version)
16 | expect(returnValue.then).to.be.a('function');
17 | expect(returnValue.catch).to.be.a('function');
18 | });
19 |
20 | it('resolves with an object with a `data` property', function (done) {
21 | request('http://example.com/')
22 | .then(function (resolvedWith) {
23 | expect(resolvedWith).to.be.an('object');
24 | expect(resolvedWith).to.include.keys('data');
25 | done();
26 | })
27 | .catch(done);
28 | })
29 |
30 | it('resolves with the response body of a GET request to its first argument', function (done) {
31 | var url = 'https://httpbin.org/get?foo=bar';
32 | request(url)
33 | .then(function (res) {
34 | var data = res.data;
35 | expect(data).to.be.a('string');
36 | data = JSON.parse(data);
37 | expect(data.url).to.equal(url);
38 | expect(data.args).to.deep.equal({ foo: 'bar' });
39 | done();
40 | })
41 | .catch(done);
42 | });
43 |
44 | it('rejects with an error when the protocol is not HTTP or HTTPS', function (done) {
45 | request('ftp://example.com')
46 | .then(function () {
47 | done(new Error('Expected Promise to be rejected'));
48 | })
49 | .catch(function (err) {
50 | try {
51 | expect(err).to.be.an.instanceOf(Error);
52 | expect(err.toString()).to.contain('protocol');
53 | expect(err.toString()).to.contain('ftp');
54 | done();
55 | } catch (e) { done(e); }
56 | })
57 | })
58 | });
59 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var qs = require('./lib/querystring');
4 | var request = require('./lib/request');
5 | var parsers = require('./lib/parsers');
6 | var dateUtil = require('./lib/date-util');
7 |
8 | var BASE = 'https://open-api.bahn.de/bin/rest.exe';
9 |
10 | var RE_STATION_ID = /^\d{9}$/;
11 |
12 | function fahrplan(key) {
13 | if (!key) throw new Error('No API key provided');
14 |
15 | function findStation(query) {
16 | return request(
17 | BASE + '/location.name?' + qs.stringify({
18 | authKey: key,
19 | input: query,
20 | format: 'json',
21 | }))
22 | .then(function (res) { res.api = api; return res; })
23 | .then(parsers.station);
24 | }
25 | function getStation(query) {
26 | return findStation(query)
27 | .then(function (result) {
28 | if (result.stations.length) return result.stations[0];
29 | return null;
30 | });
31 | }
32 | function findServices(type, query, date) {
33 | var endpoint;
34 | if (type === 'departures') endpoint = '/departureBoard';
35 | else if (type === 'arrivals') endpoint = '/arrivalBoard';
36 | else throw new Error('Type must be either "departures" or "arrivals"');
37 |
38 | if (!date) date = Date.now();
39 |
40 | // We want to support station names as well as IDs, but the API only
41 | // officially supports IDs.
42 | // The API supports querying for things that aren't IDs, but the behaviour
43 | // is not documented and surprising (e.g. searching for "B") only returns
44 | // results for Berlin Südkreuz, not Berlin Hbf.
45 | // For predictable behaviour, we'll pass anything that doesn't look like an
46 | // ID through getStation() first.
47 | // In addition, we also support passing station objects (any object with an
48 | // `id` or `name` property and Promises that resolve to them
49 | var station;
50 | if (query.id && RE_STATION_ID.test(query.id)) {
51 | // An object with an `id` property that looks like a station ID
52 | station = query;
53 | } else if (RE_STATION_ID.test(query)) {
54 | // A string that looks like a station ID
55 | station = { id: query };
56 | } else if (typeof query.then === 'function') {
57 | // A Promise, let's hope it resolves to a station
58 | station = query;
59 | } else if (query.name) {
60 | // An object with a `name` property,
61 | // let's use that to look up a station id
62 | station = getStation(query.name);
63 | } else {
64 | // Last resort, let's make sure it's a string and look it up
65 | station = getStation('' + query);
66 | }
67 |
68 | // Whatever we have now is either something that has an id property
69 | // or will (hopefully) resolve to something that has an id property.
70 | // Resolve it if it needs resolving, then look up a timetable for it.
71 | return Promise.resolve(station)
72 | .then(function (station) {
73 | return request(
74 | BASE + endpoint + '?' + qs.stringify({
75 | authKey: key,
76 | id: station.id,
77 | date: dateUtil.formatDate(date),
78 | time: dateUtil.formatTime(date),
79 | format: 'json',
80 | })
81 | )
82 | })
83 | .then(function (res) { res.api = api; return res; })
84 | .then(parsers.stationBoard);
85 | }
86 | function getItinerary(url) {
87 | return request(url)
88 | .then(function (res) { res.api = api; return res; })
89 | .then(parsers.itinerary);
90 | }
91 |
92 | var api = {
93 | station: {
94 | find: findStation,
95 | get: getStation,
96 | },
97 | departure: {
98 | find: function(stationId, date) { return findServices('departures', stationId, date) },
99 | },
100 | arrival: {
101 | find: function(stationId, date) { return findServices('arrivals', stationId, date) },
102 | },
103 | itinerary: {
104 | get: getItinerary,
105 | },
106 | };
107 | return api;
108 | }
109 |
110 | module.exports = fahrplan;
111 |
--------------------------------------------------------------------------------
/lib/parsers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var dateUtil = require('./date-util');
4 |
5 | var ARRIVAL = 'ARRIVAL';
6 | var DEPARTURE = 'DEPARTURE';
7 |
8 | function parseLocation(location, api) {
9 | var result = {};
10 | result.name = location.name;
11 | result.latitude = parseFloat(location.lat);
12 | result.longitude = parseFloat(location.lon);
13 | if (location.id) result.id = location.id;
14 | if (location.type) result.type = location.type;
15 |
16 | if (api && typeof api !== 'number') {
17 | result.departure = { find: function (date) { return api.departure.find(result.id, date) } };
18 | result.arrival = { find: function (date) { return api.arrival.find(result.id, date) } };
19 | }
20 | return result;
21 | }
22 | function parseBoardEntry(entry, type) {
23 | var result = {};
24 | result.name = entry.name;
25 | result.type = entry.type;
26 | result.station = { name: entry.stop, id: entry.stopid };
27 | if (type === ARRIVAL) result.arrival = dateUtil.parse(entry.date, entry.time);
28 | if (type === DEPARTURE) result.departure = dateUtil.parse(entry.date, entry.time);
29 | if (entry.origin) result.origin = entry.origin;
30 | if (entry.direction) result.destination = entry.direction;
31 | result.platform = entry.track;
32 | return result;
33 | }
34 | function parseItineraryMetadata(input) {
35 | var result = {};
36 | Object.keys(input).forEach(function (key) {
37 | if (!input.hasOwnProperty(key)) return;
38 | var value = input[key];
39 |
40 | if (key === 'routeIdxFrom') result.fromIndex = parseInt(value, 10);
41 | else if (key === 'routeIdxTo') result.toIndex = parseInt(value, 10);
42 | else if (key === 'priority') result.priority = parseInt(value, 10);
43 | else if (key === '$') result.description = value;
44 | else result[key] = value;
45 | });
46 | return result;
47 | }
48 |
49 | module.exports = {
50 | station: function (res) {
51 | if (!res.hasOwnProperty('data')) throw new Error('Expected a response object with a data property');
52 | var data = JSON.parse(res.data);
53 |
54 | var stops = data.LocationList.StopLocation || [];
55 | var places = data.LocationList.CoordLocation || [];
56 | if (!stops.hasOwnProperty('length')) stops = [ stops ];
57 | if (!places.hasOwnProperty('length')) places = [ places ];
58 |
59 | var result = {
60 | stations: stops.map(function (stop) { return parseLocation(stop, res.api) }),
61 | places: places.map(parseLocation),
62 | };
63 | return result;
64 | },
65 |
66 | stationBoard: function (res) {
67 | if (!res.hasOwnProperty('data')) throw new Error('Expected a response object with a data property');
68 | var data = JSON.parse(res.data);
69 |
70 | var trains, type, error;
71 | if (data.ArrivalBoard) {
72 | trains = data.ArrivalBoard.Arrival;
73 | type = ARRIVAL;
74 | } else if (data.DepartureBoard) {
75 | trains = data.DepartureBoard.Departure;
76 | type = DEPARTURE;
77 | } else if (data.Error) {
78 | error = new Error('API Error (' + data.Error.code + ')');
79 | error.code = data.Error.code;
80 | error.data = data.Error;
81 | } else {
82 | throw new Error('Expected an ArrivalBoard or DepartureBoard, got ' + data);
83 | }
84 |
85 | if (!trains) trains = [];
86 | if (!trains.hasOwnProperty('length')) trains = [ trains ];
87 |
88 | return trains.map(function (train) {
89 | var parsed = parseBoardEntry(train, type);
90 | if (res.api && train.JourneyDetailRef) {
91 | parsed.itinerary = { get: function () { return res.api.itinerary.get(train.JourneyDetailRef.ref) } };
92 | }
93 | return parsed;
94 | });
95 | },
96 |
97 | itinerary: function (res) {
98 | if (!res.hasOwnProperty('data')) throw new Error('Expected a response object with a data property');
99 | var data = JSON.parse(res.data);
100 |
101 | var stops, names, types, operators, notes;
102 | try { stops = data.JourneyDetail.Stops.Stop; } catch (e) { stops = []; }
103 | try { names = data.JourneyDetail.Names.Name; } catch (e) { names = []; }
104 | try { types = data.JourneyDetail.Types.Type; } catch (e) { types = []; }
105 | try { operators = data.JourneyDetail.Operators.Operator; } catch (e) { operators = []; }
106 | try { notes = data.JourneyDetail.Notes.Note; } catch (e) { notes = []; }
107 | if (!stops.hasOwnProperty('length')) stops = [ stops ];
108 | if (!names.hasOwnProperty('length')) names = [ names ];
109 | if (!types.hasOwnProperty('length')) types = [ types ];
110 | if (!operators.hasOwnProperty('length')) operators = [ operators ];
111 | if (!notes.hasOwnProperty('length')) notes = [ notes ];
112 |
113 | stops = stops.map(function (stop) {
114 | var result = {
115 | station: parseLocation(stop, res.api),
116 | index: parseInt(stop.routeIdx),
117 | platform: stop.track,
118 | };
119 | if (stop.depTime) result.departure = dateUtil.parse(stop.depDate, stop.depTime);
120 | if (stop.arrTime) result.arrival = dateUtil.parse(stop.arrDate, stop.arrTime);
121 |
122 | return result;
123 | });
124 |
125 | return {
126 | stops: stops,
127 | names: names.map(parseItineraryMetadata),
128 | types: types.map(parseItineraryMetadata),
129 | operators: operators.map(parseItineraryMetadata),
130 | notes: notes.map(parseItineraryMetadata),
131 | };
132 | },
133 | }
134 |
--------------------------------------------------------------------------------
/dist/fahrplan.min.js:
--------------------------------------------------------------------------------
1 | !function(r,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Fahrplan=t():r.Fahrplan=t()}(this,function(){return function(r){function t(n){if(e[n])return e[n].exports;var o=e[n]={exports:{},id:n,loaded:!1};return r[n].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var e={};return t.m=r,t.c=e,t.p="",t(0)}([function(r,t,e){!function(t,e){r.exports=e()}(this,function(){return function(r){function t(n){if(e[n])return e[n].exports;var o=e[n]={exports:{},id:n,loaded:!1};return r[n].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var e={};return t.m=r,t.c=e,t.p="",t(0)}([function(r,t,e){!function(t,e){r.exports=e()}(this,function(){return function(r){function t(n){if(e[n])return e[n].exports;var o=e[n]={exports:{},id:n,loaded:!1};return r[n].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var e={};return t.m=r,t.c=e,t.p="",t(0)}([function(r,t,e){!function(t,e){r.exports=e()}(this,function(){return function(r){function t(n){if(e[n])return e[n].exports;var o=e[n]={exports:{},id:n,loaded:!1};return r[n].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var e={};return t.m=r,t.c=e,t.p="",t(0)}([function(r,t,e){"use strict";function n(r){function t(t){return a(s+"/location.name?"+o.stringify({authKey:r,input:t,format:"json"})).then(function(r){return r.api=f,r}).then(i.station)}function e(r){return t(r).then(function(r){return r.stations.length?r.stations[0]:null})}function n(t,n,c){var d;if("departures"===t)d="/departureBoard";else{if("arrivals"!==t)throw new Error('Type must be either "departures" or "arrivals"');d="/arrivalBoard"}c||(c=Date.now());var l;return l=n.id&&u.test(n.id)?n:u.test(n)?{id:n}:"function"==typeof n.then?n:e(n.name?n.name:""+n),Promise.resolve(l).then(function(t){return a(s+d+"?"+o.stringify({authKey:r,id:t.id,date:p.formatDate(c),time:p.formatTime(c),format:"json"}))}).then(function(r){return r.api=f,r}).then(i.stationBoard)}function c(r){return a(r).then(function(r){return r.api=f,r}).then(i.itinerary)}if(!r)throw new Error("No API key provided");var f={station:{find:t,get:e},departure:{find:function(r,t){return n("departures",r,t)}},arrival:{find:function(r,t){return n("arrivals",r,t)}},itinerary:{get:c}};return f}var o=e(1),a=e(2),i=e(4),p=e(5),s="http://open-api.bahn.de/bin/rest.exe",u=/^\d{9}$/;r.exports=n},function(r,t){"use strict";r.exports={stringify:function(r,t,e){function n(r,t){return encodeURIComponent(r)+e+encodeURIComponent(t)}t||(t="&"),e||(e="=");var o=[],a=Object.keys(r);return a.forEach(function(t){var e=r[t];Array.isArray(e)?e.forEach(function(r){o.push(n(t,r))}):o.push(n(t,e))}),o.join(t)}}},function(r,t,e){"use strict";r.exports=e(3)},function(r,t){"use strict";r.exports=function(r){return new Promise(function(t,e){var n=new XMLHttpRequest,o=r.split(":").shift().toLowerCase();if("https"!==o&&"http"!==o)throw new Error("Unsupported protocol ("+o+")");n.open("GET",r,!0),n.onload=function(){t({data:n.responseText})},n.onerror=e,n.send()})}},function(r,t,e){"use strict";function n(r,t){var e={};return e.name=r.name,e.latitude=parseFloat(r.lat),e.longitude=parseFloat(r.lon),r.id&&(e.id=r.id),r.type&&(e.type=r.type),t&&"number"!=typeof t&&(e.departure={find:function(r){return t.departure.find(e.id,r)}},e.arrival={find:function(r){return t.arrival.find(e.id,r)}}),e}function o(r,t){var e={};return e.name=r.name,e.type=r.type,e.station={name:r.stop,id:r.stopid},t===p&&(e.arrival=i.parse(r.date,r.time)),t===s&&(e.departure=i.parse(r.date,r.time)),r.origin&&(e.origin=r.origin),r.direction&&(e.destination=r.direction),e.platform=r.track,e}function a(r){var t={};return Object.keys(r).forEach(function(e){if(r.hasOwnProperty(e)){var n=r[e];"routeIdxFrom"===e?t.fromIndex=parseInt(n,10):"routeIdxTo"===e?t.toIndex=parseInt(n,10):"priority"===e?t.priority=parseInt(n,10):"$"===e?t.description=n:t[e]=n}}),t}var i=e(5),p="ARRIVAL",s="DEPARTURE";r.exports={station:function(r){if(!r.hasOwnProperty("data"))throw new Error("Expected a response object with a data property");var t=JSON.parse(r.data),e=t.LocationList.StopLocation||[],o=t.LocationList.CoordLocation||[];e.hasOwnProperty("length")||(e=[e]),o.hasOwnProperty("length")||(o=[o]);var a={stations:e.map(function(t){return n(t,r.api)}),places:o.map(n)};return a},stationBoard:function(r){if(!r.hasOwnProperty("data"))throw new Error("Expected a response object with a data property");var t,e,n,a=JSON.parse(r.data);if(a.ArrivalBoard)t=a.ArrivalBoard.Arrival,e=p;else if(a.DepartureBoard)t=a.DepartureBoard.Departure,e=s;else{if(!a.Error)throw new Error("Expected an ArrivalBoard or DepartureBoard, got "+a);n=new Error("API Error ("+a.Error.code+")"),n.code=a.Error.code,n.data=a.Error}return t||(t=[]),t.hasOwnProperty("length")||(t=[t]),t.map(function(t){var n=o(t,e);return r.api&&t.JourneyDetailRef&&(n.itinerary={get:function(){return r.api.itinerary.get(t.JourneyDetailRef.ref)}}),n})},itinerary:function(r){if(!r.hasOwnProperty("data"))throw new Error("Expected a response object with a data property");var t,e,o,p,s,u=JSON.parse(r.data);try{t=u.JourneyDetail.Stops.Stop}catch(c){t=[]}try{e=u.JourneyDetail.Names.Name}catch(c){e=[]}try{o=u.JourneyDetail.Types.Type}catch(c){o=[]}try{p=u.JourneyDetail.Operators.Operator}catch(c){p=[]}try{s=u.JourneyDetail.Notes.Note}catch(c){s=[]}return t.hasOwnProperty("length")||(t=[t]),e.hasOwnProperty("length")||(e=[e]),o.hasOwnProperty("length")||(o=[o]),p.hasOwnProperty("length")||(p=[p]),s.hasOwnProperty("length")||(s=[s]),t=t.map(function(t){var e={station:n(t,r.api),index:parseInt(t.routeIdx),platform:t.track};return t.depTime&&(e.departure=i.parse(t.depDate,t.depTime)),t.arrTime&&(e.arrival=i.parse(t.arrDate,t.arrTime)),e}),{stops:t,names:e.map(a),types:o.map(a),operators:p.map(a),notes:s.map(a)}}}},function(r,t){"use strict";function e(r,t){for(void 0===t&&(t=2),r=""+r;r.length= date.valueOf()).to.be.true;
127 | });
128 | done();
129 | })
130 | .catch(done);
131 | });
132 | });
133 |
134 | describe('#arrival.find()', function () {
135 | it('resolves with an array of "arrivals"', function (done) {
136 | fahrplan.arrival.find(berlinId)
137 | .then(function (arrivals) {
138 | expect(arrivals).to.have.length.above(0);
139 | arrivals.forEach(function (arrival) {
140 | expect(arrival).to.contain.keys('name', 'type', 'station', 'arrival', 'origin');
141 | expect(arrival.arrival).to.be.an.instanceOf(Date);
142 | });
143 | done();
144 | })
145 | .catch(done);
146 | });
147 |
148 | it('accepts station IDs, station names, station objects, and promises resolving to station objects', function (done) {
149 | Promise.all([
150 | fahrplan.station.get('B')
151 | .then(function (station) { return fahrplan.arrival.find(station.id) }),
152 | fahrplan.arrival.find('B'),
153 | fahrplan.station.get('B')
154 | .then(function (station) { return fahrplan.arrival.find(station) }),
155 | fahrplan.arrival.find(fahrplan.station.get('B'))
156 | ])
157 | .then(function (results) {
158 | var byName = results[0];
159 | var byId = results[1];
160 | var byObject = results[2];
161 | var byPromise = results[3];
162 | // Can't use deep.equal because itinerary.get aren't equal
163 | var keys = [ 'name', 'type', 'station', 'arrival', 'origin', 'platform' ];
164 | byName.forEach(function (station, i) {
165 | keys.forEach(function (key) {
166 | expect(byName[i][key]).to.deep.equal(byId[i][key]);
167 | expect(byObject[i][key]).to.deep.equal(byId[i][key]);
168 | expect(byPromise[i][key]).to.deep.equal(byId[i][key]);
169 | });
170 | });
171 | done();
172 | })
173 | .catch(done);
174 | });
175 |
176 | it('accepts a date as a second argument', function (done) {
177 | var date = new Date(Date.now() + 24 * 60 * 60 * 1000);
178 | fahrplan.arrival.find(berlinId, date)
179 | .then(function (arrivals) {
180 | expect(arrivals).to.have.length.above(0);
181 | arrivals.forEach(function (arrival) {
182 | expect(arrival.arrival.valueOf() >= date.valueOf()).to.be.true;
183 | });
184 | done();
185 | })
186 | .catch(done);
187 | });
188 | });
189 |
190 | it('returns chainable promises', function (done) {
191 | fahrplan.station.find('Berlin')
192 | .then(function (stations) { return stations.stations[0].departure.find() })
193 | .then(function (departures) { return departures[0].itinerary.get() })
194 | .then(function (itinerary) {
195 | expect(itinerary.stops).to.have.length.above(0);
196 | done();
197 | })
198 | .catch(done);
199 | });
200 | });
201 |
--------------------------------------------------------------------------------
/test/data/arrivals-berlin.json:
--------------------------------------------------------------------------------
1 | {
2 | "ArrivalBoard":{
3 | "noNamespaceSchemaLocation":"http://open-api.bahn.de/bin/rest.exe/v1.0/xsd?name=hafasRestArrivalBoard.xsd",
4 | "Arrival":[{
5 | "name":"ICE 1045",
6 | "type":"ICE",
7 | "stopid":"8098160",
8 | "stop":"Berlin Hbf (tief)",
9 | "time":"12:09",
10 | "date":"2016-02-27",
11 | "origin":"Köln Hbf",
12 | "track":"7 D - G",
13 | "JourneyDetailRef":{
14 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=788346%2F263313%2F467666%2F28949%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
15 | }
16 | },{
17 | "name":"ICE 1630",
18 | "type":"ICE",
19 | "stopid":"8098160",
20 | "stop":"Berlin Hbf (tief)",
21 | "time":"12:19",
22 | "date":"2016-02-27",
23 | "origin":"Ostseebad Binz",
24 | "track":"3",
25 | "JourneyDetailRef":{
26 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=588612%2F197434%2F404710%2F6151%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
27 | }
28 | },{
29 | "name":"ICE 1209",
30 | "type":"ICE",
31 | "stopid":"8098160",
32 | "stop":"Berlin Hbf (tief)",
33 | "time":"12:19",
34 | "date":"2016-02-27",
35 | "origin":"Hamburg-Altona",
36 | "track":"2",
37 | "JourneyDetailRef":{
38 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=845295%2F286626%2F389154%2F87188%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
39 | }
40 | },{
41 | "name":"ICE 694",
42 | "type":"ICE",
43 | "stopid":"8011160",
44 | "stop":"Berlin Hbf",
45 | "time":"12:25",
46 | "date":"2016-02-27",
47 | "origin":"Stuttgart Hbf",
48 | "track":"12",
49 | "JourneyDetailRef":{
50 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=421260%2F144243%2F168354%2F56243%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8011160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
51 | }
52 | },{
53 | "name":"IC 2286",
54 | "type":"IC",
55 | "stopid":"8098160",
56 | "stop":"Berlin Hbf (tief)",
57 | "time":"12:30",
58 | "date":"2016-02-27",
59 | "origin":"Leipzig Hbf",
60 | "track":"7",
61 | "JourneyDetailRef":{
62 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=251742%2F86184%2F14814%2F76507%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
63 | }
64 | },{
65 | "name":"ICE 1034",
66 | "type":"ICE",
67 | "stopid":"8098160",
68 | "stop":"Berlin Hbf (tief)",
69 | "time":"12:34",
70 | "date":"2016-02-27",
71 | "origin":"Berlin Südkreuz",
72 | "track":"8",
73 | "JourneyDetailRef":{
74 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=506826%2F169460%2F474730%2F68423%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
75 | }
76 | },{
77 | "name":"EC 176",
78 | "type":"EC",
79 | "stopid":"8098160",
80 | "stop":"Berlin Hbf (tief)",
81 | "time":"12:58",
82 | "date":"2016-02-27",
83 | "origin":"Praha hl.n.",
84 | "track":"8",
85 | "JourneyDetailRef":{
86 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=787785%2F267644%2F518638%2F3276%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
87 | }
88 | },{
89 | "name":"ICE 1535",
90 | "type":"ICE",
91 | "stopid":"8098160",
92 | "stop":"Berlin Hbf (tief)",
93 | "time":"13:01",
94 | "date":"2016-02-27",
95 | "origin":"Frankfurt(Main)Hbf",
96 | "track":"7",
97 | "JourneyDetailRef":{
98 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=747543%2F250257%2F746152%2F123895%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
99 | }
100 | },{
101 | "name":"ICE 545",
102 | "type":"ICE",
103 | "stopid":"8098160",
104 | "stop":"Berlin Hbf (tief)",
105 | "time":"13:07",
106 | "date":"2016-02-27",
107 | "origin":"Köln Hbf",
108 | "track":"2 D - G",
109 | "JourneyDetailRef":{
110 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=163389%2F57658%2F428194%2F159634%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
111 | }
112 | },{
113 | "name":"EC 54",
114 | "type":"EC",
115 | "stopid":"8011160",
116 | "stop":"Berlin Hbf",
117 | "time":"13:15",
118 | "date":"2016-02-27",
119 | "origin":"Gdynia Glowna",
120 | "track":"14",
121 | "JourneyDetailRef":{
122 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=802554%2F273023%2F268466%2F133285%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8011160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
123 | }
124 | },{
125 | "name":"ICE 707",
126 | "type":"ICE",
127 | "stopid":"8098160",
128 | "stop":"Berlin Hbf (tief)",
129 | "time":"13:21",
130 | "date":"2016-02-27",
131 | "origin":"Hamburg-Altona",
132 | "track":"1",
133 | "JourneyDetailRef":{
134 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=167454%2F59689%2F915872%2F402118%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
135 | }
136 | },{
137 | "name":"IC 141",
138 | "type":"IC",
139 | "stopid":"8011160",
140 | "stop":"Berlin Hbf",
141 | "time":"13:22",
142 | "date":"2016-02-27",
143 | "origin":"Amsterdam Centraal",
144 | "track":"11",
145 | "JourneyDetailRef":{
146 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=23307%2F12725%2F945890%2F465176%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8011160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
147 | }
148 | },{
149 | "name":"IC 2387",
150 | "type":"IC",
151 | "stopid":"8098160",
152 | "stop":"Berlin Hbf (tief)",
153 | "time":"13:23",
154 | "date":"2016-02-27",
155 | "origin":"Rostock Hbf",
156 | "track":"2",
157 | "JourneyDetailRef":{
158 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=529689%2F179041%2F286824%2F33151%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
159 | }
160 | },{
161 | "name":"ICE 374",
162 | "type":"ICE",
163 | "stopid":"8011160",
164 | "stop":"Berlin Hbf",
165 | "time":"13:28",
166 | "date":"2016-02-27",
167 | "origin":"Basel SBB",
168 | "track":"12",
169 | "JourneyDetailRef":{
170 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=552960%2F187102%2F860974%2F246167%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8011160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
171 | }
172 | },{
173 | "name":"ICE 1682",
174 | "type":"ICE",
175 | "stopid":"8098160",
176 | "stop":"Berlin Hbf (tief)",
177 | "time":"13:33",
178 | "date":"2016-02-27",
179 | "origin":"München Hbf",
180 | "track":"8",
181 | "JourneyDetailRef":{
182 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=433320%2F145741%2F120422%2F84229%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
183 | }
184 | },{
185 | "name":"ICE 847",
186 | "type":"ICE",
187 | "stopid":"8098160",
188 | "stop":"Berlin Hbf (tief)",
189 | "time":"14:09",
190 | "date":"2016-02-27",
191 | "origin":"Köln Hbf",
192 | "track":"6 D - G",
193 | "JourneyDetailRef":{
194 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=602841%2F205169%2F177206%2F112344%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
195 | }
196 | },{
197 | "name":"ICE 1683",
198 | "type":"ICE",
199 | "stopid":"8098160",
200 | "stop":"Berlin Hbf (tief)",
201 | "time":"14:19",
202 | "date":"2016-02-27",
203 | "origin":"Hamburg-Altona",
204 | "track":"1",
205 | "JourneyDetailRef":{
206 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=413592%2F139169%2F129598%2F73065%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
207 | }
208 | },{
209 | "name":"IC 2252",
210 | "type":"IC",
211 | "stopid":"8098160",
212 | "stop":"Berlin Hbf (tief)",
213 | "time":"14:19",
214 | "date":"2016-02-27",
215 | "origin":"Ostseebad Binz",
216 | "track":"2",
217 | "JourneyDetailRef":{
218 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=366450%2F124344%2F536970%2F146335%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
219 | }
220 | },{
221 | "name":"ICE 692",
222 | "type":"ICE",
223 | "stopid":"8011160",
224 | "stop":"Berlin Hbf",
225 | "time":"14:25",
226 | "date":"2016-02-27",
227 | "origin":"Stuttgart Hbf",
228 | "track":"12",
229 | "JourneyDetailRef":{
230 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=773886%2F261759%2F554812%2F19444%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8011160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
231 | }
232 | },{
233 | "name":"ICE 1036",
234 | "type":"ICE",
235 | "stopid":"8098160",
236 | "stop":"Berlin Hbf (tief)",
237 | "time":"14:30",
238 | "date":"2016-02-27",
239 | "origin":"Leipzig Hbf",
240 | "track":"7",
241 | "JourneyDetailRef":{
242 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=553947%2F185170%2F47566%2F160866%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Darr%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
243 | }
244 | }]
245 | }
246 | }
--------------------------------------------------------------------------------
/test/data/departures-berlin.json:
--------------------------------------------------------------------------------
1 | {
2 | "DepartureBoard":{
3 | "noNamespaceSchemaLocation":"http://open-api.bahn.de/bin/rest.exe/v1.0/xsd?name=hafasRestDepartureBoard.xsd",
4 | "Departure":[{
5 | "name":"ICE 1209",
6 | "type":"ICE",
7 | "stopid":"8098160",
8 | "stop":"Berlin Hbf (tief)",
9 | "time":"12:27",
10 | "date":"2016-02-27",
11 | "direction":"Innsbruck Hbf",
12 | "track":"2",
13 | "JourneyDetailRef":{
14 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=506907%2F173830%2F684408%2F173235%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
15 | }
16 | },{
17 | "name":"ICE 373",
18 | "type":"ICE",
19 | "stopid":"8011160",
20 | "stop":"Berlin Hbf",
21 | "time":"12:31",
22 | "date":"2016-02-27",
23 | "direction":"Interlaken Ost",
24 | "track":"14",
25 | "JourneyDetailRef":{
26 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=239691%2F85231%2F45290%2F57252%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8011160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
27 | }
28 | },{
29 | "name":"IC 144",
30 | "type":"IC",
31 | "stopid":"8011160",
32 | "stop":"Berlin Hbf",
33 | "time":"12:34",
34 | "date":"2016-02-27",
35 | "direction":"Amsterdam Centraal",
36 | "track":"13",
37 | "JourneyDetailRef":{
38 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=736869%2F250629%2F800338%2F154547%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8011160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
39 | }
40 | },{
41 | "name":"EC 45",
42 | "type":"EC",
43 | "stopid":"8011160",
44 | "stop":"Berlin Hbf",
45 | "time":"12:37",
46 | "date":"2016-02-27",
47 | "direction":"Warszawa Wschodnia",
48 | "track":"11",
49 | "JourneyDetailRef":{
50 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=356913%2F124410%2F147466%2F45238%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8011160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
51 | }
52 | },{
53 | "name":"ICE 1034",
54 | "type":"ICE",
55 | "stopid":"8098160",
56 | "stop":"Berlin Hbf (tief)",
57 | "time":"12:38",
58 | "date":"2016-02-27",
59 | "direction":"Hamburg-Altona",
60 | "track":"8",
61 | "JourneyDetailRef":{
62 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=349107%2F116887%2F267770%2F17516%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
63 | }
64 | },{
65 | "name":"ICE 548",
66 | "type":"ICE",
67 | "stopid":"8098160",
68 | "stop":"Berlin Hbf (tief)",
69 | "time":"12:52",
70 | "date":"2016-02-27",
71 | "direction":"Köln Hbf",
72 | "track":"4 D - G",
73 | "JourneyDetailRef":{
74 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=900402%2F303346%2F205962%2F197153%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
75 | }
76 | },{
77 | "name":"EC 175",
78 | "type":"EC",
79 | "stopid":"8098160",
80 | "stop":"Berlin Hbf (tief)",
81 | "time":"13:00",
82 | "date":"2016-02-27",
83 | "direction":"Praha hl.n.",
84 | "track":"2",
85 | "JourneyDetailRef":{
86 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=281808%2F98982%2F676006%2F244067%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
87 | }
88 | },{
89 | "name":"ICE 1630",
90 | "type":"ICE",
91 | "stopid":"8098160",
92 | "stop":"Berlin Hbf (tief)",
93 | "time":"13:03",
94 | "date":"2016-02-27",
95 | "direction":"Frankfurt(Main)Hbf",
96 | "track":"3",
97 | "JourneyDetailRef":{
98 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=174783%2F59491%2F153628%2F18553%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
99 | }
100 | },{
101 | "name":"ICE 1535",
102 | "type":"ICE",
103 | "stopid":"8098160",
104 | "stop":"Berlin Hbf (tief)",
105 | "time":"13:04",
106 | "date":"2016-02-27",
107 | "direction":"Ostseebad Binz",
108 | "track":"7",
109 | "JourneyDetailRef":{
110 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=884805%2F296011%2F984184%2F197157%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
111 | }
112 | },{
113 | "name":"EC 176",
114 | "type":"EC",
115 | "stopid":"8098160",
116 | "stop":"Berlin Hbf (tief)",
117 | "time":"13:07",
118 | "date":"2016-02-27",
119 | "direction":"Hamburg-Altona",
120 | "track":"8",
121 | "JourneyDetailRef":{
122 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=262443%2F92530%2F49052%2F62955%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
123 | }
124 | },{
125 | "name":"IC 2387",
126 | "type":"IC",
127 | "stopid":"8098160",
128 | "stop":"Berlin Hbf (tief)",
129 | "time":"13:30",
130 | "date":"2016-02-27",
131 | "direction":"Leipzig Hbf",
132 | "track":"2",
133 | "JourneyDetailRef":{
134 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=710199%2F239211%2F433064%2F20201%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
135 | }
136 | },{
137 | "name":"ICE 691",
138 | "type":"ICE",
139 | "stopid":"8011160",
140 | "stop":"Berlin Hbf",
141 | "time":"13:34",
142 | "date":"2016-02-27",
143 | "direction":"Stuttgart Hbf",
144 | "track":"13",
145 | "JourneyDetailRef":{
146 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=25548%2F12307%2F816314%2F399641%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8011160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
147 | }
148 | },{
149 | "name":"ICE 1682",
150 | "type":"ICE",
151 | "stopid":"8098160",
152 | "stop":"Berlin Hbf (tief)",
153 | "time":"13:42",
154 | "date":"2016-02-27",
155 | "direction":"Hamburg-Altona",
156 | "track":"8",
157 | "JourneyDetailRef":{
158 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=945222%2F316375%2F527524%2F51312%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
159 | }
160 | },{
161 | "name":"ICE 848",
162 | "type":"ICE",
163 | "stopid":"8098160",
164 | "stop":"Berlin Hbf (tief)",
165 | "time":"13:49",
166 | "date":"2016-02-27",
167 | "direction":"Köln Hbf",
168 | "track":"2 D - G",
169 | "JourneyDetailRef":{
170 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=19215%2F10632%2F936396%2F461793%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
171 | }
172 | },{
173 | "name":"ICE 1683",
174 | "type":"ICE",
175 | "stopid":"8098160",
176 | "stop":"Berlin Hbf (tief)",
177 | "time":"14:28",
178 | "date":"2016-02-27",
179 | "direction":"München Hbf",
180 | "track":"1",
181 | "JourneyDetailRef":{
182 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=937230%2F313715%2F845724%2F110452%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
183 | }
184 | },{
185 | "name":"ICE 375",
186 | "type":"ICE",
187 | "stopid":"8011160",
188 | "stop":"Berlin Hbf",
189 | "time":"14:31",
190 | "date":"2016-02-27",
191 | "direction":"Basel SBB",
192 | "track":"14",
193 | "JourneyDetailRef":{
194 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=120861%2F43080%2F325604%2F122515%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8011160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
195 | }
196 | },{
197 | "name":"IC 142",
198 | "type":"IC",
199 | "stopid":"8011160",
200 | "stop":"Berlin Hbf",
201 | "time":"14:34",
202 | "date":"2016-02-27",
203 | "direction":"Amsterdam Centraal",
204 | "track":"13",
205 | "JourneyDetailRef":{
206 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=329754%2F114886%2F937050%2F358607%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8011160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
207 | }
208 | },{
209 | "name":"EC 55",
210 | "type":"EC",
211 | "stopid":"8011160",
212 | "stop":"Berlin Hbf",
213 | "time":"14:37",
214 | "date":"2016-02-27",
215 | "direction":"Gdynia Glowna",
216 | "track":"11",
217 | "JourneyDetailRef":{
218 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=52764%2F23098%2F48564%2F6694%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8011160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
219 | }
220 | },{
221 | "name":"ICE 800",
222 | "type":"ICE",
223 | "stopid":"8098160",
224 | "stop":"Berlin Hbf (tief)",
225 | "time":"14:39",
226 | "date":"2016-02-27",
227 | "direction":"Hamburg-Altona",
228 | "track":"8",
229 | "JourneyDetailRef":{
230 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=142248%2F51525%2F610594%2F257881%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
231 | }
232 | },{
233 | "name":"ICE 546",
234 | "type":"ICE",
235 | "stopid":"8098160",
236 | "stop":"Berlin Hbf (tief)",
237 | "time":"14:52",
238 | "date":"2016-02-27",
239 | "direction":"Köln Hbf",
240 | "track":"4 D - G",
241 | "JourneyDetailRef":{
242 | "ref":"http://open-api.bahn.de/bin/rest.exe/v1.0/journeyDetail?ref=326058%2F111885%2F661630%2F222129%2F80%3Fdate%3D2016-02-27%26station_evaId%3D8098160%26station_type%3Ddep%26authKey%3DDBhackFrankfurt0316%26lang%3Den%26format%3Djson%26"
243 | }
244 | }]
245 | }
246 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fahrplan.js
2 |
3 | A JavaScript client for Deutsche Bahn's [timetable API](http://data.deutschebahn.com/apis/fahrplan/).
4 |
5 | ```js
6 | const fahrplan = require('fahrplan')('TopSecretAPIKey');
7 |
8 | fahrplan.station.get('Berlin')
9 | .then(berlin => berlin.departure.find())
10 | .then(departures => {
11 | console.log('The next train is %s to %s', departures[0].name, departures[0].destination);
12 | return departures[0].itinerary.get();
13 | })
14 | .then(itinerary => {
15 | console.log('It calls at:');
16 | itinerary.stops.forEach(stop => console.log(stop.station.name));
17 | });
18 |
19 | fahrplan.arrival.find('Berlin Hbf', new Date(2016, 0, 1))
20 | .then(arrivals => console.log('The first train of the year was %s from %s', arrivals[0].name, arrivals[0].origin));
21 | ```
22 |
23 | It runs in node.js and the browser (well, sort of).
24 |
25 | ## Installing
26 |
27 | ### node.js
28 |
29 | ```sh
30 | npm install fahrplan
31 | ```
32 |
33 | ### Browser
34 |
35 | You can use Fahrplan.js with a bundler like [Webpack](http://webpack.github.io) or [Browserify](http://browserify.org) (`npm install fahrplan`) or by downloading and including [fahrplan.js](https://raw.githubusercontent.com/pbock/fahrplan/master/dist/fahrplan.js) or [fahrplan.min.js](https://raw.githubusercontent.com/pbock/fahrplan/master/dist/fahrplan.min.js) directly.
36 |
37 | In the latter case, you will need a polyfill for Promises unless you can live [without support for Internet Explorer](http://caniuse.com/#feat=promises). [es6-promise](https://github.com/stefanpenner/es6-promise) is a good one.
38 |
39 | Fahrplan.js works in the browser, but you can't use it yet because DB's server doesn't send an `Access-Control-Allow-Origin` header. This will hopefully be sorted soon, in the meantime, you can test it in Chrome by starting it with the ominously named `--disable-web-security` flag.
40 |
41 | ## Usage
42 |
43 | Create a new instance of the client with your API key:
44 |
45 | ```js
46 | const fahrplan = require('fahrplan')(/* Your API Key goes here */);
47 | // Or in the browser, if you're not using a bundler:
48 | var fahrplan = Fahrplan(/* Your API Key goes here */);
49 | ```
50 |
51 | Just [send an email to dbopendata@deutschebahn.com](mailto:dbopendata@deutschebahn.com) to get an API key. There have been reports of the key being leaked, but we couldn't possibly comment.
52 |
53 | There are currently only three things the API lets you do:
54 |
55 | * `station.find()`/`station.get()`:
56 | Search for a station by name (similar to the booking form on [bahn.de](http://www.bahn.de/p/view/index.shtml))
57 | * `departure.find()`/`arrival.find()`:
58 | Find all trains leaving from/arriving at a station at a given time
59 | * `itinerary.get()`:
60 | Find out at which stations a service calls and additional information
61 |
62 | ### `station.find(name)`
63 |
64 | Starts a full-text search for the given `name` and resolves with a list of matching stations and places.
65 |
66 | Example:
67 | ```js
68 | fahrplan.station.find('Hamburg').then(doSomethingWithTheResult);
69 | fahrplan.station.find('KA').then(doSomethingWithTheResult);
70 | fahrplan.station.find('008010255').then(doSomethingWithTheResult);
71 | ```
72 |
73 | **Returns** a Promise that resolves with an object like this:
74 |
75 | ```js
76 | {
77 | stations: [
78 | {
79 | name: 'Berlin Hbf',
80 | latitude: 52.525589,
81 | longitude: 13.369548,
82 | id: '008011160',
83 | departure: { find: function () { /* … */ } },
84 | arrival: { find: function () { /* … */ } }
85 | },
86 | // …
87 | ],
88 | places: [
89 | {
90 | name: 'Berlin, Lido Kultur- + Veranstaltungs GmbH (Kultu',
91 | latitude: 52.499169
92 | longitude: 13.444968,
93 | type: 'POI'
94 | },
95 | // …
96 | ]
97 | }
98 | ```
99 |
100 | ### `station.get(name)`
101 |
102 | Starts a full-text search for the given name and resolves with only the first matched station, or `null` if no station was found.
103 |
104 | Behaves like `station.find()`, but only resolves with the first match or `null`.
105 | (Note that the search is annoyingly tolerant and will try to return a result even for the silliest of queries.)
106 |
107 | Example:
108 | ```js
109 | fahrplan.station.get('München Hbf').then(doSomethingWithMunich);
110 | ```
111 |
112 | **Returns** a Promise that resolves with a `station` object like this:
113 |
114 | ```js
115 | {
116 | name: 'München Hbf',
117 | latitude: 48.140228,
118 | longitude: 11.558338,
119 | id: '008000261',
120 | departure: { find: function () { /* … */ } },
121 | arrival: { find: function () { /* … */ } }
122 | }
123 | ```
124 |
125 | ### `departure.find(station, [date])`
126 |
127 | Looks up all trains leaving from the station `station` at the given `date` (defaults to now).
128 |
129 | `station` can be:
130 |
131 | - a Station ID (recommended),
132 | - a `station` object from `station.find()` or `station.get()`,
133 | - a Promise that resolves to a station,
134 | - or anything that `station.get()` understands.
135 |
136 | IDs will be passed straight through to the API, Promises will be resolved. Station names will go through `station.get()`, causing an additional HTTP request. For faster results and lower traffic, it's best to use an ID if you know it.
137 |
138 | ```js
139 | // All trains leaving from Berlin Ostbahnhof right now
140 | fahrplan.departure.find('008010255').then(doSomethingWithTheResult);
141 | // Find the first train that left from Berlin Hbf in 2016
142 | fahrplan.departure.find('Berlin Hbf', new Date(2016, 0, 1)).then(departures => departures[0]);
143 | // You can also use a Promise as the first parameter.
144 | fahrplan.departure.find(fahrplan.station.get('Münster')).then(doSomethingWithTheResult);
145 | ```
146 |
147 | **Returns** a Promise that resolves with an array like this:
148 | ```js
149 | [
150 | {
151 | name: 'ICE 1586',
152 | type: 'ICE',
153 | station: { name: 'Berlin Hbf (tief)', id: '8098160' },
154 | departure: new Date('Fri Feb 26 2016 19:42:00 GMT+0100 (CET)'),
155 | destination: 'Hamburg-Altona',
156 | platform: '7',
157 | itinerary: { get: function () { /* … */ } }
158 | },
159 | // …
160 | ]
161 | ```
162 |
163 | Because you'll often look up a station before you can fetch the departures board, you can also fetch the departures right from the result of `station.find()` or `station.get()`. Station objects come with a `departure.find([ date ])` method that works just the same.
164 |
165 | Example:
166 |
167 | ```js
168 | fahrplan.station.get('Köln')
169 | .then(cologne => cologne.departure.find())
170 | .then(doSomethingWithTheDeparturesBoard);
171 | ```
172 |
173 | ### `arrival.find(station, [date])`
174 |
175 | Looks up all trains leaving from the station `stationId` at the given `date` (defaults to now).
176 |
177 | `station` can be:
178 |
179 | - a Station ID (recommended),
180 | - a `station` object from `station.find()` or `station.get()`,
181 | - a Promise that resolves to a station,
182 | - or anything that `station.get()` understands.
183 |
184 | IDs will be passed straight through to the API, Promises will be resolved. Station names will go through `station.get()`, causing an additional HTTP request. For faster results and lower traffic, it's best to use an ID if you know it.
185 |
186 | Example:
187 |
188 | ```js
189 | // All trains arriving in Berlin Ostbahnhof right now
190 | fahrplan.arrival.find('Berlin Ostbahnhof').then(doSomethingWithTheResult);
191 | // Find the first train that arrived in Berlin Hbf in 2016
192 | fahrplan.arrival.find('008011160', new Date(2016, 0, 1)).then(arrivals => arrivals[0]);
193 | // You can also use a Promise as the first parameter.
194 | fahrplan.arrival.find(fahrplan.station.get('Duisburg')).then(doSomethingWithTheResult);
195 | ```
196 |
197 | **Returns** a Promise that resolves with an array just like the one in `departure.find()`, except that `destination` and `departure` are replaced with `origin` and `arrival`, respectively.
198 |
199 | Because you'll often look up a station before you can fetch the arrivals board, you can also fetch the arrivals right from the result of `station.find()` or `station.get()`. Station objects come with a `arrival.find([ date ])` method that works just the same.
200 |
201 | ### `itinerary.get(url)`
202 |
203 | Gets the itinerary and additional information for a given service. Because the URLs involved are stateful (so much for being a REST API), it doesn't make sense to call this method directly. Instead, you can call it from the result of `departure.find()` or `arrival.find()`.
204 |
205 | Example:
206 | ```js
207 | fahrplan.arrival.find('008010255')
208 | .then(arrivals => arrivals[0].itinerary.get())
209 | .then(doSomethingWithTheItinerary);
210 | ```
211 |
212 | **Returns** a Promise that resolves with an array like this:
213 |
214 | ```js
215 | {
216 | stops: [
217 | {
218 | station: {
219 | name: 'Hamburg-Altona',
220 | latitude: 53.552696,
221 | longitude: 9.935174,
222 | id: '8002553',
223 | departure: { find: function () { /* … */ }},
224 | arrival: { find: function () { /* … */ }}
225 | },
226 | index: 0,
227 | platform: '10',
228 | departure: new Date('2016-02-26T17:19:00.000Z'),
229 | },
230 | // … (8 more)
231 | ],
232 | names: [
233 | { name: 'ICE 903', fromIndex: 0, toIndex: 8 }
234 | ],
235 | types: [
236 | { type: 'ICE', fromIndex: 0, toIndex: 8 }
237 | ],
238 | operators: [
239 | { name: 'DPN', fromIndex: 0, toIndex: 8 }
240 | ],
241 | notes: [
242 | { key: 'BR', priority: 450, fromIndex: 0, toIndex: 8, description: 'Bordrestaurant' }
243 | ]
244 | }
245 | ```
246 |
247 | ### Known issues
248 |
249 | #### Error handling
250 |
251 | The client doesn't throw on all API errors yet, you may sometimes get an empty result when you should get an error.
252 |
253 | #### Timezone support
254 |
255 | All dates and times are assumed to be in the timezone of your machine. This is fine for most of the queries you will want to do, but it means that you can run into trouble if your computer is not in Central European Time.
256 |
257 | There's no easy fix for this, partly because JavaScript's timezone handling is atrocious, but mainly because the API doesn't return unambiguous times anyway – **all API results are in local time** which needn't always be CET/CEST.
258 |
259 | The API currently only returns trains that run in/through Germany, but that's enough to cause issues: The EN 23 Москва́-Белору́сская—Strasbourg runs through Germany and is therefore included in the results. It leaves Moscow at 19:15 UTC – but the API doesn't tell you that, it tells you that it leaves at 22:15 and lets you guess the timezone.
260 |
261 | One *could* guess the timezone by [abusing the station codes](https://github.com/pbock/fahrplan/commit/e2d0803f91d6db9335e7c2e06bd09936c1756caa#commitcomment-16570880) or the `latitude`/`longitude` information, but that seems overkill for something that *should* be fixed by sending all the necessary data.
262 |
263 | Timezones really aren't a problem that hasn't been solved yet, and as soon as Deutsche Bahn includes timezones in its API results, this client will support them too.
264 |
--------------------------------------------------------------------------------
/test/lib/parsers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var expect = require('chai').expect;
4 | var fs = require('fs');
5 | var pr = require('path').resolve;
6 |
7 | var parsers = require('../../lib/parsers');
8 | var dateUtil = require('../../lib/date-util');
9 |
10 | function responseObjectTest(fn) {
11 | return function () {
12 | expect(function () { fn() }).to.throw();
13 | expect(function () { fn({}) }).to.throw();
14 | expect(function () { fn(1234) }).to.throw();
15 | expect(function () { fn({ data: 'Not valid JSON' }) }).to.throw();
16 | }
17 | }
18 |
19 | describe('parsers', function () {
20 | describe('#station()', function () {
21 | var p = parsers.station;
22 | it('expects a { data: \'{"some JSON":"data"}\' } object', responseObjectTest(p));
23 |
24 | var berlinData = {
25 | data: fs.readFileSync(pr(__dirname, '../data/station-berlin.json')).toString(),
26 | };
27 | var searchByIdData = {
28 | data: fs.readFileSync(pr(__dirname, '../data/station-008011160.json')).toString(),
29 | };
30 | var testData = {
31 | data: JSON.stringify({
32 | LocationList: {
33 | StopLocation: [
34 | { name: 'Test station 1', lat: '3.141592', lon: '-42.2', id: '01234567' },
35 | ],
36 | CoordLocation: [],
37 | },
38 | }),
39 | };
40 | var testDataWithAPI = {
41 | data: testData.data,
42 | api: {
43 | departure: { find: function (id, date) { return 'findDeparture' + id + date } },
44 | arrival: { find: function (id, date) { return 'findArrival' + id + date } },
45 | },
46 | };
47 |
48 | it('returns an object with "stations" and "places" arrays', function () {
49 | var parsed = p(berlinData);
50 | expect(parsed).to.include.keys('stations', 'places');
51 | expect(parsed.stations).to.be.an('array');
52 | expect(parsed.places).to.be.an('array');
53 | });
54 |
55 | it('returns as many stations and coordinates as passed in the JSON', function () {
56 | var parsed = p(berlinData);
57 | expect(parsed.stations.length).to.equal(9);
58 | expect(parsed.places.length).to.equal(41);
59 | });
60 |
61 | it('converts { lat , lon } to { latitude , longitude }', function () {
62 | var parsed = p(testData);
63 | var stop = parsed.stations[0];
64 | expect(stop.latitude).to.be.a('number');
65 | expect(stop.longitude).to.be.a('number');
66 | expect(stop.latitude).to.equal(3.141592);
67 | expect(stop.longitude).to.equal(-42.2);
68 | });
69 |
70 | it('leaves the "name", "type" and "id" properties intact', function () {
71 | var parsed = p(testData);
72 | var stop = parsed.stations[0];
73 | expect(stop.name).to.equal('Test station 1');
74 | expect(stop.id).to.equal('01234567')
75 | expect(stop.type).to.be.undefined;
76 | });
77 |
78 | it('also works when the input "StopLocation"/"CoordLocation" is a single object rather than an array', function () {
79 | // These are returned when searching for a station ID rather than a string
80 | var parsed = p(searchByIdData);
81 | expect(parsed.stations).to.be.an('array');
82 | expect(parsed.places).to.be.an('array');
83 | expect(parsed.stations[0].name).to.equal('Berlin Hbf');
84 | });
85 |
86 | it('also works when the input "StopLocation"/"CoordLocation" is undefined', function () {
87 | var parsed = p({ data: JSON.stringify({ LocationList: {} })});
88 | expect(parsed.stations).to.deep.equal([]);
89 | expect(parsed.places).to.deep.equal([]);
90 | });
91 |
92 | it('adds "departure.find" and "arrival.find" methods if an API reference was provided', function () {
93 | var parsed = p(testDataWithAPI);
94 | expect(parsed.stations[0].departure.find).to.be.a('function');
95 | expect(parsed.stations[0].arrival.find).to.be.a('function');
96 | expect(parsed.stations[0].departure.find('foo')).to.equal('findDeparture01234567foo');
97 | expect(parsed.stations[0].arrival.find('bar')).to.equal('findArrival01234567bar');
98 | });
99 | });
100 |
101 | describe('#stationBoard()', function () {
102 | var p = parsers.stationBoard;
103 | it('expects a { data: \'{"some JSON":"data"}\' } object', responseObjectTest(p));
104 |
105 | var berlinArrivals = {
106 | data: fs.readFileSync(pr(__dirname, '../data/arrivals-berlin.json')),
107 | };
108 | var berlinArrivalsWithAPI = {
109 | data: fs.readFileSync(pr(__dirname, '../data/arrivals-berlin.json')),
110 | api: { itinerary: { get: function (url) { return 'getItinerary' + url } } },
111 | };
112 | var berlinDepartures = {
113 | data: fs.readFileSync(pr(__dirname, '../data/departures-berlin.json')),
114 | };
115 | var moskvaDeparture = {
116 | data: fs.readFileSync(pr(__dirname, '../data/departure-moskva.json')),
117 | };
118 |
119 | it('returns an array', function () {
120 | expect(p(berlinDepartures)).to.be.an('array');
121 | expect(p(berlinArrivals)).to.be.an('array');
122 | });
123 |
124 | it('returns as many entries as there are in the input JSON', function () {
125 | expect(p(berlinDepartures).length).to.equal(20);
126 | expect(p(berlinArrivals).length).to.equal(20);
127 | expect(p({ data: JSON.stringify({ ArrivalBoard: { Arrival: [] } }) }).length).to.equal(0);
128 | expect(p({ data: JSON.stringify({ DepartureBoard: { Departure: [] } }) }).length).to.equal(0);
129 | });
130 |
131 | it('converts the "date" and "time" properties to "arrival" and "departure" Date instances as appropriate', function () {
132 | expect(p(berlinDepartures)[0].departure).to.be.an.instanceOf(Date);
133 | expect(p(berlinDepartures)[0].arrival).to.be.undefined;
134 | expect(p(berlinArrivals)[0].departure).to.be.undefined;
135 | expect(p(berlinArrivals)[0].arrival).to.be.an.instanceOf(Date);
136 | expect(p(berlinDepartures)[0].departure).to.deep.equal(new Date(2016, 1, 27, 12, 27));
137 | expect(p(berlinArrivals)[0].arrival).to.deep.equal(new Date(2016, 1, 27, 12, 9));
138 | });
139 |
140 | it('turns "stop" and "stopid" into a "station" object', function () {
141 | p(berlinDepartures).forEach(function (departure) {
142 | expect(departure.station.id).to.be.oneOf([ '8098160', '8011160' ]);
143 | expect(departure.station.name).to.be.oneOf([ 'Berlin Hbf', 'Berlin Hbf (tief)' ]);
144 | expect(departure.stop).to.be.undefined;
145 | expect(departure.stopid).to.be.undefined;
146 | });
147 | });
148 |
149 | it('has "name", "type", and "platform" properties, and "origin" or "destination" as appropriate', function () {
150 | p(berlinDepartures).forEach(function (departure) {
151 | expect(departure).to.include.keys('name', 'type', 'platform', 'destination');
152 | });
153 | p(berlinArrivals).forEach(function (arrival) {
154 | expect(arrival).to.include.keys('name', 'type', 'platform', 'origin');
155 | });
156 | });
157 |
158 | it('also works when the input "Departure" or "Arrival" is a single object rather than an array', function () {
159 | var parsed = p(moskvaDeparture);
160 | expect(parsed.length).to.equal(1);
161 | var dep = parsed[0];
162 | expect(dep.name).to.equal('EN 23');
163 | expect(dep.station).to.deep.equal({ name: 'Moskva Belorusskaja', id: '2000058' });
164 | });
165 |
166 | it('sets all the properties correctly', function () {
167 | var input = {
168 | name: 'ICE 1234',
169 | type: 'ICE',
170 | stopid: '01234567',
171 | stop: 'Earth',
172 | time: '13:37',
173 | date: '2016-02-29',
174 | direction: 'Jupiter',
175 | track: '42',
176 | };
177 | var output = p({ data: JSON.stringify({ DepartureBoard: { Departure: [input] } }) })[0];
178 |
179 | expect(output.name).to.equal(input.name);
180 | expect(output.type).to.equal(input.type);
181 | expect(output.station.name).to.equal(input.stop);
182 | expect(output.station.id).to.equal(input.stopid);
183 | expect(output.destination).to.equal(input.direction);
184 | expect(output.platform).to.equal(input.track);
185 | expect(output.departure).to.deep.equal(dateUtil.parse(input.date, input.time));
186 | });
187 |
188 | it('adds an "itinerary.get" method if request reference was provided', function () {
189 | var arrivals = p(berlinArrivalsWithAPI);
190 | arrivals.forEach(function (arrival) {
191 | expect(arrival.itinerary.get).to.be.a('function');
192 | expect(arrival.itinerary.get()).to.match(/^getItineraryhttp/);
193 | });
194 | });
195 | });
196 |
197 | describe('#itinerary()', function () {
198 | var p = parsers.itinerary;
199 | it('expects a { data: \'{"some JSON":"data"}\' } object', responseObjectTest(p));
200 |
201 | var ic142 = { data: fs.readFileSync(pr(__dirname, '../data/itinerary-ic142.json')).toString() };
202 | var simpleItinerary = {
203 | Stops: {
204 | Stop: [
205 | {
206 | name: 'First stop',
207 | id: '08',
208 | lon: '12.34',
209 | lat: '-87.65',
210 | routeIdx: '0',
211 | depTime: '00:00',
212 | depDate: '2020-05-05',
213 | },
214 | {
215 | name: 'Last stop',
216 | id: '09',
217 | lon: '23.45',
218 | lat: '-76.54',
219 | routeIdx: '1',
220 | arrTime: '13:00',
221 | arrDate: '2020-05-05',
222 | },
223 | ],
224 | },
225 | Names: {
226 | Name: {
227 | name: 'IC 123',
228 | routeIdxFrom: '0',
229 | routeIdxTo: '1',
230 | },
231 | },
232 | Types: {
233 | Type: {
234 | type: 'IC',
235 | routeIdxFrom: '0',
236 | routeIdxTo: '1',
237 | },
238 | },
239 | Operators: {
240 | Operator: {
241 | name: 'NASA',
242 | routeIdxFrom: '0',
243 | routeIdxTo: '1',
244 | },
245 | },
246 | Notes: {
247 | Note: {
248 | mostProperties: 'will just get passed straight through',
249 | except: '$, which becomes',
250 | $: 'description',
251 | },
252 | },
253 | };
254 | var simple = { data: JSON.stringify({ JourneyDetail: simpleItinerary }) };
255 | var simpleWithApi = {
256 | data: JSON.stringify({ JourneyDetail: simpleItinerary }),
257 | api: {
258 | departure: { find: function (id, date) { return 'findDeparture' + id + date } },
259 | arrival: { find: function (id, date) { return 'findArrival' + id + date } },
260 | },
261 | };
262 |
263 | it('returns an object with "stops", "names", "types", "operators" and "notes" arrays', function () {
264 | var itinerary = p(ic142);
265 | expect(itinerary.stops).to.be.an('array');
266 | expect(itinerary.names).to.be.an('array');
267 | expect(itinerary.types).to.be.an('array');
268 | expect(itinerary.operators).to.be.an('array');
269 | expect(itinerary.notes).to.be.an('array');
270 | });
271 |
272 | it('returns as many "stops" as there are in the input', function () {
273 | var itinerary = p(ic142);
274 | expect(itinerary.stops.length).to.equal(JSON.parse(ic142.data).JourneyDetail.Stops.Stop.length);
275 | });
276 |
277 | it('converts the "stops" correctly', function () {
278 | var itinerary = p(simple);
279 | expect(itinerary.stops.length).to.equal(2);
280 | itinerary.stops.forEach(function (output, i) {
281 | var input = simpleItinerary.Stops.Stop[i];
282 | expect(output.station.name).to.equal(input.name);
283 | expect(output.station.id).to.equal(input.id);
284 | expect(output.station.latitude).to.equal(parseFloat(input.lat));
285 | expect(output.station.longitude).to.equal(parseFloat(input.lon));
286 | expect(output.index).to.equal(parseInt(input.routeIdx));
287 | if (input.arrTime) {
288 | expect(output.arrival).to.deep.equal(dateUtil.parse(input.arrDate, input.arrTime));
289 | }
290 | if (input.depTime) {
291 | expect(output.departure).to.deep.equal(dateUtil.parse(input.depDate, input.depTime));
292 | }
293 | })
294 | });
295 |
296 | it('cleans up the metadata', function () {
297 | var itinerary = p(simple);
298 | expect(itinerary.names[0]).to.deep.equal({ name: 'IC 123', fromIndex: 0, toIndex: 1 });
299 | expect(itinerary.types[0]).to.deep.equal({ type: 'IC', fromIndex: 0, toIndex: 1 });
300 | expect(itinerary.operators[0]).to.deep.equal({ name: 'NASA', fromIndex: 0, toIndex: 1 });
301 | expect(itinerary.notes[0]).to.deep.equal({
302 | mostProperties: 'will just get passed straight through',
303 | except: '$, which becomes',
304 | description: 'description',
305 | });
306 | });
307 |
308 |
309 | it('adds "departures.get" and "arrivals.get" methods if an API reference was provided', function () {
310 | var parsed = p(simpleWithApi);
311 | expect(parsed.stops[0].station.departure.find('foo')).to.equal('findDeparture08foo');
312 | expect(parsed.stops[0].station.arrival.find('bar')).to.equal('findArrival08bar');
313 | expect(parsed.stops[1].station.departure.find('foo')).to.equal('findDeparture09foo');
314 | expect(parsed.stops[1].station.arrival.find('bar')).to.equal('findArrival09bar');
315 | });
316 |
317 | });
318 | });
319 |
--------------------------------------------------------------------------------
/dist/fahrplan.js:
--------------------------------------------------------------------------------
1 | (function webpackUniversalModuleDefinition(root, factory) {
2 | if(typeof exports === 'object' && typeof module === 'object')
3 | module.exports = factory();
4 | else if(typeof define === 'function' && define.amd)
5 | define([], factory);
6 | else if(typeof exports === 'object')
7 | exports["Fahrplan"] = factory();
8 | else
9 | root["Fahrplan"] = factory();
10 | })(this, function() {
11 | return /******/ (function(modules) { // webpackBootstrap
12 | /******/ // The module cache
13 | /******/ var installedModules = {};
14 |
15 | /******/ // The require function
16 | /******/ function __webpack_require__(moduleId) {
17 |
18 | /******/ // Check if module is in cache
19 | /******/ if(installedModules[moduleId])
20 | /******/ return installedModules[moduleId].exports;
21 |
22 | /******/ // Create a new module (and put it into the cache)
23 | /******/ var module = installedModules[moduleId] = {
24 | /******/ exports: {},
25 | /******/ id: moduleId,
26 | /******/ loaded: false
27 | /******/ };
28 |
29 | /******/ // Execute the module function
30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
31 |
32 | /******/ // Flag the module as loaded
33 | /******/ module.loaded = true;
34 |
35 | /******/ // Return the exports of the module
36 | /******/ return module.exports;
37 | /******/ }
38 |
39 |
40 | /******/ // expose the modules object (__webpack_modules__)
41 | /******/ __webpack_require__.m = modules;
42 |
43 | /******/ // expose the module cache
44 | /******/ __webpack_require__.c = installedModules;
45 |
46 | /******/ // __webpack_public_path__
47 | /******/ __webpack_require__.p = "";
48 |
49 | /******/ // Load entry module and return exports
50 | /******/ return __webpack_require__(0);
51 | /******/ })
52 | /************************************************************************/
53 | /******/ ([
54 | /* 0 */
55 | /***/ function(module, exports, __webpack_require__) {
56 |
57 | (function webpackUniversalModuleDefinition(root, factory) {
58 | if(true)
59 | module.exports = factory();
60 | else if(typeof define === 'function' && define.amd)
61 | define([], factory);
62 | else if(typeof exports === 'object')
63 | exports["Fahrplan"] = factory();
64 | else
65 | root["Fahrplan"] = factory();
66 | })(this, function() {
67 | return /******/ (function(modules) { // webpackBootstrap
68 | /******/ // The module cache
69 | /******/ var installedModules = {};
70 |
71 | /******/ // The require function
72 | /******/ function __webpack_require__(moduleId) {
73 |
74 | /******/ // Check if module is in cache
75 | /******/ if(installedModules[moduleId])
76 | /******/ return installedModules[moduleId].exports;
77 |
78 | /******/ // Create a new module (and put it into the cache)
79 | /******/ var module = installedModules[moduleId] = {
80 | /******/ exports: {},
81 | /******/ id: moduleId,
82 | /******/ loaded: false
83 | /******/ };
84 |
85 | /******/ // Execute the module function
86 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
87 |
88 | /******/ // Flag the module as loaded
89 | /******/ module.loaded = true;
90 |
91 | /******/ // Return the exports of the module
92 | /******/ return module.exports;
93 | /******/ }
94 |
95 |
96 | /******/ // expose the modules object (__webpack_modules__)
97 | /******/ __webpack_require__.m = modules;
98 |
99 | /******/ // expose the module cache
100 | /******/ __webpack_require__.c = installedModules;
101 |
102 | /******/ // __webpack_public_path__
103 | /******/ __webpack_require__.p = "";
104 |
105 | /******/ // Load entry module and return exports
106 | /******/ return __webpack_require__(0);
107 | /******/ })
108 | /************************************************************************/
109 | /******/ ([
110 | /* 0 */
111 | /***/ function(module, exports, __webpack_require__) {
112 |
113 | (function webpackUniversalModuleDefinition(root, factory) {
114 | if(true)
115 | module.exports = factory();
116 | else if(typeof define === 'function' && define.amd)
117 | define([], factory);
118 | else if(typeof exports === 'object')
119 | exports["Fahrplan"] = factory();
120 | else
121 | root["Fahrplan"] = factory();
122 | })(this, function() {
123 | return /******/ (function(modules) { // webpackBootstrap
124 | /******/ // The module cache
125 | /******/ var installedModules = {};
126 |
127 | /******/ // The require function
128 | /******/ function __webpack_require__(moduleId) {
129 |
130 | /******/ // Check if module is in cache
131 | /******/ if(installedModules[moduleId])
132 | /******/ return installedModules[moduleId].exports;
133 |
134 | /******/ // Create a new module (and put it into the cache)
135 | /******/ var module = installedModules[moduleId] = {
136 | /******/ exports: {},
137 | /******/ id: moduleId,
138 | /******/ loaded: false
139 | /******/ };
140 |
141 | /******/ // Execute the module function
142 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
143 |
144 | /******/ // Flag the module as loaded
145 | /******/ module.loaded = true;
146 |
147 | /******/ // Return the exports of the module
148 | /******/ return module.exports;
149 | /******/ }
150 |
151 |
152 | /******/ // expose the modules object (__webpack_modules__)
153 | /******/ __webpack_require__.m = modules;
154 |
155 | /******/ // expose the module cache
156 | /******/ __webpack_require__.c = installedModules;
157 |
158 | /******/ // __webpack_public_path__
159 | /******/ __webpack_require__.p = "";
160 |
161 | /******/ // Load entry module and return exports
162 | /******/ return __webpack_require__(0);
163 | /******/ })
164 | /************************************************************************/
165 | /******/ ([
166 | /* 0 */
167 | /***/ function(module, exports, __webpack_require__) {
168 |
169 | 'use strict';
170 |
171 | var qs = __webpack_require__(1);
172 | var request = __webpack_require__(2);
173 | var parsers = __webpack_require__(4);
174 | var dateUtil = __webpack_require__(5);
175 |
176 | var BASE = 'http://open-api.bahn.de/bin/rest.exe';
177 |
178 | var RE_STATION_ID = /^\d{9}$/;
179 |
180 | function fahrplan(key) {
181 | if (!key) throw new Error('No API key provided');
182 |
183 | function findStation(query) {
184 | return request(
185 | BASE + '/location.name?' + qs.stringify({
186 | authKey: key,
187 | input: query,
188 | format: 'json',
189 | }))
190 | .then(function (res) { res.api = api; return res; })
191 | .then(parsers.station);
192 | }
193 | function getStation(query) {
194 | return findStation(query)
195 | .then(function (result) {
196 | if (result.stations.length) return result.stations[0];
197 | return null;
198 | });
199 | }
200 | function findServices(type, query, date) {
201 | var endpoint;
202 | if (type === 'departures') endpoint = '/departureBoard';
203 | else if (type === 'arrivals') endpoint = '/arrivalBoard';
204 | else throw new Error('Type must be either "departures" or "arrivals"');
205 |
206 | if (!date) date = Date.now();
207 |
208 | // We want to support station names as well as IDs, but the API only
209 | // officially supports IDs.
210 | // The API supports querying for things that aren't IDs, but the behaviour
211 | // is not documented and surprising (e.g. searching for "B") only returns
212 | // results for Berlin Südkreuz, not Berlin Hbf.
213 | // For predictable behaviour, we'll pass anything that doesn't look like an
214 | // ID through getStation() first.
215 | // In addition, we also support passing station objects (any object with an
216 | // `id` or `name` property and Promises that resolve to them
217 | var station;
218 | if (query.id && RE_STATION_ID.test(query.id)) {
219 | // An object with an `id` property that looks like a station ID
220 | station = query;
221 | } else if (RE_STATION_ID.test(query)) {
222 | // A string that looks like a station ID
223 | station = { id: query };
224 | } else if (typeof query.then === 'function') {
225 | // A Promise, let's hope it resolves to a station
226 | station = query;
227 | } else if (query.name) {
228 | // An object with a `name` property,
229 | // let's use that to look up a station id
230 | station = getStation(query.name);
231 | } else {
232 | // Last resort, let's make sure it's a string and look it up
233 | station = getStation('' + query);
234 | }
235 |
236 | // Whatever we have now is either something that has an id property
237 | // or will (hopefully) resolve to something that has an id property.
238 | // Resolve it if it needs resolving, then look up a timetable for it.
239 | return Promise.resolve(station)
240 | .then(function (station) {
241 | return request(
242 | BASE + endpoint + '?' + qs.stringify({
243 | authKey: key,
244 | id: station.id,
245 | date: dateUtil.formatDate(date),
246 | time: dateUtil.formatTime(date),
247 | format: 'json',
248 | })
249 | )
250 | })
251 | .then(function (res) { res.api = api; return res; })
252 | .then(parsers.stationBoard);
253 | }
254 | function getItinerary(url) {
255 | return request(url)
256 | .then(function (res) { res.api = api; return res; })
257 | .then(parsers.itinerary);
258 | }
259 |
260 | var api = {
261 | station: {
262 | find: findStation,
263 | get: getStation,
264 | },
265 | departure: {
266 | find: function(stationId, date) { return findServices('departures', stationId, date) },
267 | },
268 | arrival: {
269 | find: function(stationId, date) { return findServices('arrivals', stationId, date) },
270 | },
271 | itinerary: {
272 | get: getItinerary,
273 | },
274 | };
275 | return api;
276 | }
277 |
278 | module.exports = fahrplan;
279 |
280 |
281 | /***/ },
282 | /* 1 */
283 | /***/ function(module, exports) {
284 |
285 | 'use strict';
286 |
287 | // Minimalistic re-implementation of querystring.stringify
288 |
289 | module.exports = {
290 | stringify: function (params, separator, equals) {
291 | if (!separator) separator = '&';
292 | if (!equals) equals = '=';
293 |
294 | var output = [];
295 |
296 | function serialize(key, value) {
297 | return encodeURIComponent(key) + equals + encodeURIComponent(value);
298 | }
299 |
300 | var keys = Object.keys(params);
301 | keys.forEach(function (key) {
302 | var value = params[key];
303 |
304 | if (Array.isArray(value)) {
305 | value.forEach(function (arrayValue) {
306 | output.push(serialize(key, arrayValue));
307 | });
308 | } else {
309 | output.push(serialize(key, value));
310 | }
311 | });
312 |
313 | return output.join(separator);
314 | },
315 | }
316 |
317 |
318 | /***/ },
319 | /* 2 */
320 | /***/ function(module, exports, __webpack_require__) {
321 |
322 | 'use strict';
323 |
324 | if (true) module.exports = __webpack_require__(3);
325 | else module.exports = require('./node');
326 |
327 |
328 | /***/ },
329 | /* 3 */
330 | /***/ function(module, exports) {
331 |
332 | 'use strict';
333 |
334 | /* global Promise */
335 |
336 | module.exports = function request(url) {
337 | return new Promise(function (resolve, reject) {
338 | var req = new XMLHttpRequest();
339 | var protocol = url.split(':').shift().toLowerCase();
340 | if (protocol !== 'https' && protocol !== 'http') {
341 | throw new Error('Unsupported protocol (' + protocol + ')');
342 | }
343 |
344 | req.open('GET', url, true);
345 |
346 | req.onload = function () {
347 | resolve({ data: req.responseText });
348 | }
349 |
350 | req.onerror = reject;
351 |
352 | req.send();
353 | });
354 | }
355 |
356 |
357 | /***/ },
358 | /* 4 */
359 | /***/ function(module, exports, __webpack_require__) {
360 |
361 | 'use strict';
362 |
363 | var dateUtil = __webpack_require__(5);
364 |
365 | var ARRIVAL = 'ARRIVAL';
366 | var DEPARTURE = 'DEPARTURE';
367 |
368 | function parseLocation(location, api) {
369 | var result = {};
370 | result.name = location.name;
371 | result.latitude = parseFloat(location.lat);
372 | result.longitude = parseFloat(location.lon);
373 | if (location.id) result.id = location.id;
374 | if (location.type) result.type = location.type;
375 |
376 | if (api && typeof api !== 'number') {
377 | result.departure = { find: function (date) { return api.departure.find(result.id, date) } };
378 | result.arrival = { find: function (date) { return api.arrival.find(result.id, date) } };
379 | }
380 | return result;
381 | }
382 | function parseBoardEntry(entry, type) {
383 | var result = {};
384 | result.name = entry.name;
385 | result.type = entry.type;
386 | result.station = { name: entry.stop, id: entry.stopid };
387 | if (type === ARRIVAL) result.arrival = dateUtil.parse(entry.date, entry.time);
388 | if (type === DEPARTURE) result.departure = dateUtil.parse(entry.date, entry.time);
389 | if (entry.origin) result.origin = entry.origin;
390 | if (entry.direction) result.destination = entry.direction;
391 | result.platform = entry.track;
392 | return result;
393 | }
394 | function parseItineraryMetadata(input) {
395 | var result = {};
396 | Object.keys(input).forEach(function (key) {
397 | if (!input.hasOwnProperty(key)) return;
398 | var value = input[key];
399 |
400 | if (key === 'routeIdxFrom') result.fromIndex = parseInt(value, 10);
401 | else if (key === 'routeIdxTo') result.toIndex = parseInt(value, 10);
402 | else if (key === 'priority') result.priority = parseInt(value, 10);
403 | else if (key === '$') result.description = value;
404 | else result[key] = value;
405 | });
406 | return result;
407 | }
408 |
409 | module.exports = {
410 | station: function (res) {
411 | if (!res.hasOwnProperty('data')) throw new Error('Expected a response object with a data property');
412 | var data = JSON.parse(res.data);
413 |
414 | var stops = data.LocationList.StopLocation || [];
415 | var places = data.LocationList.CoordLocation || [];
416 | if (!stops.hasOwnProperty('length')) stops = [ stops ];
417 | if (!places.hasOwnProperty('length')) places = [ places ];
418 |
419 | var result = {
420 | stations: stops.map(function (stop) { return parseLocation(stop, res.api) }),
421 | places: places.map(parseLocation),
422 | };
423 | return result;
424 | },
425 |
426 | stationBoard: function (res) {
427 | if (!res.hasOwnProperty('data')) throw new Error('Expected a response object with a data property');
428 | var data = JSON.parse(res.data);
429 |
430 | var trains, type, error;
431 | if (data.ArrivalBoard) {
432 | trains = data.ArrivalBoard.Arrival;
433 | type = ARRIVAL;
434 | } else if (data.DepartureBoard) {
435 | trains = data.DepartureBoard.Departure;
436 | type = DEPARTURE;
437 | } else if (data.Error) {
438 | error = new Error('API Error (' + data.Error.code + ')');
439 | error.code = data.Error.code;
440 | error.data = data.Error;
441 | } else {
442 | throw new Error('Expected an ArrivalBoard or DepartureBoard, got ' + data);
443 | }
444 |
445 | if (!trains) trains = [];
446 | if (!trains.hasOwnProperty('length')) trains = [ trains ];
447 |
448 | return trains.map(function (train) {
449 | var parsed = parseBoardEntry(train, type);
450 | if (res.api && train.JourneyDetailRef) {
451 | parsed.itinerary = { get: function () { return res.api.itinerary.get(train.JourneyDetailRef.ref) } };
452 | }
453 | return parsed;
454 | });
455 | },
456 |
457 | itinerary: function (res) {
458 | if (!res.hasOwnProperty('data')) throw new Error('Expected a response object with a data property');
459 | var data = JSON.parse(res.data);
460 |
461 | var stops, names, types, operators, notes;
462 | try { stops = data.JourneyDetail.Stops.Stop; } catch (e) { stops = []; }
463 | try { names = data.JourneyDetail.Names.Name; } catch (e) { names = []; }
464 | try { types = data.JourneyDetail.Types.Type; } catch (e) { types = []; }
465 | try { operators = data.JourneyDetail.Operators.Operator; } catch (e) { operators = []; }
466 | try { notes = data.JourneyDetail.Notes.Note; } catch (e) { notes = []; }
467 | if (!stops.hasOwnProperty('length')) stops = [ stops ];
468 | if (!names.hasOwnProperty('length')) names = [ names ];
469 | if (!types.hasOwnProperty('length')) types = [ types ];
470 | if (!operators.hasOwnProperty('length')) operators = [ operators ];
471 | if (!notes.hasOwnProperty('length')) notes = [ notes ];
472 |
473 | stops = stops.map(function (stop) {
474 | var result = {
475 | station: parseLocation(stop, res.api),
476 | index: parseInt(stop.routeIdx),
477 | platform: stop.track,
478 | };
479 | if (stop.depTime) result.departure = dateUtil.parse(stop.depDate, stop.depTime);
480 | if (stop.arrTime) result.arrival = dateUtil.parse(stop.arrDate, stop.arrTime);
481 |
482 | return result;
483 | });
484 |
485 | return {
486 | stops: stops,
487 | names: names.map(parseItineraryMetadata),
488 | types: types.map(parseItineraryMetadata),
489 | operators: operators.map(parseItineraryMetadata),
490 | notes: notes.map(parseItineraryMetadata),
491 | };
492 | },
493 | }
494 |
495 |
496 | /***/ },
497 | /* 5 */
498 | /***/ function(module, exports) {
499 |
500 | 'use strict';
501 |
502 | function zeroPad(number, length) {
503 | if (length === undefined) length = 2;
504 | number = '' + number;
505 | while (number.length < length) number = '0' + number;
506 | return number;
507 | }
508 |
509 | module.exports = {
510 | formatDate: function (input) {
511 | input = new Date(input);
512 | var dd = zeroPad(input.getDate());
513 | var mm = zeroPad(input.getMonth() + 1);
514 | var yyyy = zeroPad(input.getFullYear(), 4);
515 | return [ yyyy, mm, dd ].join('-');
516 | },
517 |
518 | formatTime: function (input) {
519 | input = new Date(input);
520 | var hh = zeroPad(input.getHours());
521 | var mm = zeroPad(input.getMinutes());
522 | return hh + ':' + mm;
523 | },
524 |
525 | parse: function (date, time) {
526 | date = date.split('-');
527 | time = time.split(':');
528 |
529 | var year = parseInt(date[0], 10);
530 | var month = parseInt(date[1], 10) - 1;
531 | var day = parseInt(date[2], 10);
532 |
533 | var hour = parseInt(time[0], 10);
534 | var minute = parseInt(time[1], 10);
535 |
536 | return new Date(year, month, day, hour, minute);
537 | },
538 | }
539 |
540 |
541 | /***/ }
542 | /******/ ])
543 | });
544 | ;
545 |
546 | /***/ }
547 | /******/ ])
548 | });
549 | ;
550 |
551 | /***/ }
552 | /******/ ])
553 | });
554 | ;
--------------------------------------------------------------------------------