├── .editorconfig ├── .eslintrc ├── .gitignore ├── LICENSE.md ├── README.md ├── dist ├── fahrplan.js └── fahrplan.min.js ├── index.js ├── lib ├── date-util.js ├── parsers.js ├── querystring.js └── request │ ├── browser.js │ ├── index.js │ └── node.js ├── package.json ├── test ├── browser │ └── index.html ├── config.example.js ├── data │ ├── arrivals-berlin.json │ ├── departure-moskva.json │ ├── departures-berlin.json │ ├── itinerary-ic142.json │ ├── station-008011160.json │ └── station-berlin.json ├── index.js └── lib │ ├── date-util.js │ ├── parsers.js │ ├── querystring.js │ └── request.js └── webpack ├── config.js ├── config.min.js └── test.js /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/config.js 3 | test/browser/test.js 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ; -------------------------------------------------------------------------------- /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 (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/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fahrplan.js 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/config.example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // Add your API key here 5 | key: 'topsecret', 6 | }; 7 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /test/data/itinerary-ic142.json: -------------------------------------------------------------------------------- 1 | { 2 | "JourneyDetail":{ 3 | "noNamespaceSchemaLocation":"http://open-api.bahn.de/bin/rest.exe/v1.0/xsd?name=hafasRestJourneyDetail.xsd", 4 | "Stops":{ 5 | "Stop":[{ 6 | "name":"Berlin Ostbahnhof", 7 | "id":"8010255", 8 | "lon":"13.434567", 9 | "lat":"52.510972", 10 | "routeIdx":"0", 11 | "depTime":"14:23", 12 | "depDate":"2016-02-27", 13 | "track":"6" 14 | },{ 15 | "name":"Berlin Hbf", 16 | "id":"8011160", 17 | "lon":"13.369548", 18 | "lat":"52.525589", 19 | "routeIdx":"1", 20 | "arrTime":"14:30", 21 | "arrDate":"2016-02-27", 22 | "depTime":"14:34", 23 | "depDate":"2016-02-27", 24 | "track":"13" 25 | },{ 26 | "name":"Berlin-Spandau", 27 | "id":"8010404", 28 | "lon":"13.197530", 29 | "lat":"52.534470", 30 | "routeIdx":"2", 31 | "arrTime":"14:50", 32 | "arrDate":"2016-02-27", 33 | "depTime":"14:52", 34 | "depDate":"2016-02-27", 35 | "track":"4" 36 | },{ 37 | "name":"Stendal", 38 | "id":"8010334", 39 | "lon":"11.854407", 40 | "lat":"52.594725", 41 | "routeIdx":"3", 42 | "arrTime":"15:31", 43 | "arrDate":"2016-02-27", 44 | "depTime":"15:34", 45 | "depDate":"2016-02-27", 46 | "track":"1" 47 | },{ 48 | "name":"Wolfsburg Hbf", 49 | "id":"8006552", 50 | "lon":"10.787783", 51 | "lat":"52.429494", 52 | "routeIdx":"4", 53 | "arrTime":"16:02", 54 | "arrDate":"2016-02-27", 55 | "depTime":"16:04", 56 | "depDate":"2016-02-27", 57 | "track":"1" 58 | },{ 59 | "name":"Hannover Hbf", 60 | "id":"8000152", 61 | "lon":"9.741016", 62 | "lat":"52.376763", 63 | "routeIdx":"5", 64 | "arrTime":"16:37", 65 | "arrDate":"2016-02-27", 66 | "depTime":"16:40", 67 | "depDate":"2016-02-27", 68 | "track":"12" 69 | },{ 70 | "name":"Minden(Westf)", 71 | "id":"8000252", 72 | "lon":"8.934729", 73 | "lat":"52.290134", 74 | "routeIdx":"6", 75 | "arrTime":"17:10", 76 | "arrDate":"2016-02-27", 77 | "depTime":"17:12", 78 | "depDate":"2016-02-27", 79 | "track":"11" 80 | },{ 81 | "name":"Bünde(Westf)", 82 | "id":"8000059", 83 | "lon":"8.573875", 84 | "lat":"52.202076", 85 | "routeIdx":"7", 86 | "arrTime":"17:30", 87 | "arrDate":"2016-02-27", 88 | "depTime":"17:32", 89 | "depDate":"2016-02-27", 90 | "track":"4" 91 | },{ 92 | "name":"Osnabrück Hbf", 93 | "id":"8000294", 94 | "lon":"8.061777", 95 | "lat":"52.272848", 96 | "routeIdx":"8", 97 | "arrTime":"17:51", 98 | "arrDate":"2016-02-27", 99 | "depTime":"17:53", 100 | "depDate":"2016-02-27", 101 | "track":"12" 102 | },{ 103 | "name":"Rheine", 104 | "id":"8000316", 105 | "lon":"7.434258", 106 | "lat":"52.276300", 107 | "routeIdx":"9", 108 | "arrTime":"18:19", 109 | "arrDate":"2016-02-27", 110 | "depTime":"18:21", 111 | "depDate":"2016-02-27", 112 | "track":"2" 113 | },{ 114 | "name":"Bad Bentheim", 115 | "id":"8000879", 116 | "lon":"7.158541", 117 | "lat":"52.309848", 118 | "routeIdx":"10", 119 | "arrTime":"18:34", 120 | "arrDate":"2016-02-27", 121 | "depTime":"18:44", 122 | "depDate":"2016-02-27", 123 | "track":"2" 124 | },{ 125 | "name":"Bad Bentheim(Gr)", 126 | "id":"8000151", 127 | "lon":"7.039758", 128 | "lat":"52.309111", 129 | "routeIdx":"11", 130 | "arrTime":"18:49", 131 | "arrDate":"2016-02-27", 132 | "depTime":"18:49", 133 | "depDate":"2016-02-27" 134 | },{ 135 | "name":"Hengelo", 136 | "id":"8400316", 137 | "lon":"6.793723", 138 | "lat":"52.261854", 139 | "routeIdx":"12", 140 | "arrTime":"19:01", 141 | "arrDate":"2016-02-27", 142 | "depTime":"19:03", 143 | "depDate":"2016-02-27", 144 | "track":"2" 145 | },{ 146 | "name":"Almelo", 147 | "id":"8400051", 148 | "lon":"6.654813", 149 | "lat":"52.357284", 150 | "routeIdx":"13", 151 | "arrTime":"19:14", 152 | "arrDate":"2016-02-27", 153 | "depTime":"19:16", 154 | "depDate":"2016-02-27", 155 | "track":"4" 156 | },{ 157 | "name":"Deventer", 158 | "id":"8400173", 159 | "lon":"6.160505", 160 | "lat":"52.257432", 161 | "routeIdx":"14", 162 | "arrTime":"19:42", 163 | "arrDate":"2016-02-27", 164 | "depTime":"19:48", 165 | "depDate":"2016-02-27", 166 | "track":"3" 167 | },{ 168 | "name":"Apeldoorn", 169 | "id":"8400066", 170 | "lon":"5.967489", 171 | "lat":"52.209429", 172 | "routeIdx":"15", 173 | "arrTime":"19:59", 174 | "arrDate":"2016-02-27", 175 | "depTime":"20:00", 176 | "depDate":"2016-02-27", 177 | "track":"1" 178 | },{ 179 | "name":"Amersfoort", 180 | "id":"8400055", 181 | "lon":"5.373239", 182 | "lat":"52.153642", 183 | "routeIdx":"16", 184 | "arrTime":"20:24", 185 | "arrDate":"2016-02-27", 186 | "depTime":"20:26", 187 | "depDate":"2016-02-27", 188 | "track":"7" 189 | },{ 190 | "name":"Hilversum", 191 | "id":"8400322", 192 | "lon":"5.181400", 193 | "lat":"52.226670", 194 | "routeIdx":"17", 195 | "arrTime":"20:38", 196 | "arrDate":"2016-02-27", 197 | "depTime":"20:39", 198 | "depDate":"2016-02-27", 199 | "track":"5" 200 | },{ 201 | "name":"Amsterdam Centraal", 202 | "id":"8400058", 203 | "lon":"4.899426", 204 | "lat":"52.379190", 205 | "routeIdx":"18", 206 | "arrTime":"21:00", 207 | "arrDate":"2016-02-27", 208 | "track":"15a" 209 | }] 210 | }, 211 | "Names":{ 212 | "Name":{ 213 | "name":"IC 142", 214 | "routeIdxFrom":"0", 215 | "routeIdxTo":"18" 216 | } 217 | }, 218 | "Types":{ 219 | "Type":{ 220 | "type":"IC", 221 | "routeIdxFrom":"0", 222 | "routeIdxTo":"18" 223 | } 224 | }, 225 | "Operators":{ 226 | "Operator":{ 227 | "name":"DPN", 228 | "routeIdxFrom":"0", 229 | "routeIdxTo":"18" 230 | } 231 | }, 232 | "Notes":{ 233 | "Note":[{ 234 | "key":"G ", 235 | "priority":"260", 236 | "routeIdxFrom":"0", 237 | "routeIdxTo":"18", 238 | "$":"Number of bicycles conveyed limited" 239 | },{ 240 | "key":"FR", 241 | "priority":"260", 242 | "routeIdxFrom":"0", 243 | "routeIdxTo":"18", 244 | "$":"Bicycles conveyed - subject to reservation" 245 | },{ 246 | "key":"RE", 247 | "priority":"320", 248 | "routeIdxFrom":"0", 249 | "routeIdxTo":"18", 250 | "$":"Please reserve" 251 | },{ 252 | "key":"BW", 253 | "priority":"450", 254 | "routeIdxFrom":"11", 255 | "routeIdxTo":"18", 256 | "$":"Bar" 257 | },{ 258 | "key":"BT", 259 | "priority":"450", 260 | "routeIdxFrom":"0", 261 | "routeIdxTo":"11", 262 | "$":"Bordbistro" 263 | },{ 264 | "key":"RO", 265 | "priority":"560", 266 | "routeIdxFrom":"11", 267 | "routeIdxTo":"18", 268 | "$":"space for wheelchairs" 269 | }] 270 | } 271 | } 272 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /test/data/station-berlin.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 | "name":"Berlin Südkreuz", 11 | "lon":"13.365314", 12 | "lat":"52.475042", 13 | "id":"008011113" 14 | },{ 15 | "name":"Berlin Ostbahnhof", 16 | "lon":"13.434567", 17 | "lat":"52.510972", 18 | "id":"008010255" 19 | },{ 20 | "name":"Berlin-Spandau", 21 | "lon":"13.197530", 22 | "lat":"52.534470", 23 | "id":"008010404" 24 | },{ 25 | "name":"Berlin Gesundbrunnen", 26 | "lon":"13.388515", 27 | "lat":"52.548961", 28 | "id":"008011102" 29 | },{ 30 | "name":"Bernau(b Berlin)", 31 | "lon":"13.592292", 32 | "lat":"52.675834", 33 | "id":"008013470" 34 | },{ 35 | "name":"Berlin Wannsee", 36 | "lon":"13.179336", 37 | "lat":"52.421053", 38 | "id":"008010405" 39 | },{ 40 | "name":"Berlin-Lichtenberg", 41 | "lon":"13.496925", 42 | "lat":"52.509894", 43 | "id":"008010036" 44 | },{ 45 | "name":"BERLINO", 46 | "lon":"13.386987", 47 | "lat":"52.520501", 48 | "id":"008096003" 49 | }], 50 | "CoordLocation":[{ 51 | "name":"Berlin, Lido Kultur- + Veranstaltungs GmbH (Kultu", 52 | "lon":"13.444968", 53 | "lat":"52.499169", 54 | "type":"POI" 55 | },{ 56 | "name":"Berlin, A.b.santos (Gastronomie)", 57 | "lon":"13.382501", 58 | "lat":"52.531099", 59 | "type":"POI" 60 | },{ 61 | "name":"Berlin, A la Carté (Gastronomie)", 62 | "lon":"13.367301", 63 | "lat":"52.493695", 64 | "type":"POI" 65 | },{ 66 | "name":"Berlin, A Trane (Kultur und Unterhaltung)", 67 | "lon":"13.320197", 68 | "lat":"52.506702", 69 | "type":"POI" 70 | },{ 71 | "name":"Berlin, A + O Hotel Friedrichshain (Hotel)", 72 | "lon":"13.468600", 73 | "lat":"52.506999", 74 | "type":"POI" 75 | },{ 76 | "name":"Berlin, Aaina (Gastronomie)", 77 | "lon":"13.426800", 78 | "lat":"52.499295", 79 | "type":"POI" 80 | },{ 81 | "name":"Berlin, Aapka (Gastronomie)", 82 | "lon":"13.404696", 83 | "lat":"52.533598", 84 | "type":"POI" 85 | },{ 86 | "name":"Berlin, Aarti (Gastronomie)", 87 | "lon":"13.393999", 88 | "lat":"52.524699", 89 | "type":"POI" 90 | },{ 91 | "name":"Berlin, Abacus Tierpark Hotel (Hotel)", 92 | "lon":"13.520396", 93 | "lat":"52.500599", 94 | "type":"POI" 95 | },{ 96 | "name":"Berlin, abba Berlin Hotel (Hotel)", 97 | "lon":"13.321806", 98 | "lat":"52.499844", 99 | "type":"POI" 100 | },{ 101 | "name":"Berlin, Abendstern (Hotel)", 102 | "lon":"13.304403", 103 | "lat":"52.505696", 104 | "type":"POI" 105 | },{ 106 | "name":"Berlin, Abgedreht (Gastronomie)", 107 | "lon":"13.451799", 108 | "lat":"52.515602", 109 | "type":"POI" 110 | },{ 111 | "name":"Berlin, Abion Spreebogen Waterside Hotel (Hotel)", 112 | "lon":"13.347399", 113 | "lat":"52.524798", 114 | "type":"POI" 115 | },{ 116 | "name":"Berlin, Abirams (Gastronomie)", 117 | "lon":"13.394601", 118 | "lat":"52.492500", 119 | "type":"POI" 120 | },{ 121 | "name":"Berlin, Abraxas (Kultur und Unterhaltung)", 122 | "lon":"13.316404", 123 | "lat":"52.506100", 124 | "type":"POI" 125 | },{ 126 | "name":"Berlin, Abricot (Gastronomie)", 127 | "lon":"13.410898", 128 | "lat":"52.488697", 129 | "type":"POI" 130 | },{ 131 | "name":"Berlin, Acapulco (Gastronomie)", 132 | "lon":"13.445003", 133 | "lat":"52.516797", 134 | "type":"POI" 135 | },{ 136 | "name":"Berlin, Ach! Niko! Ach! (Gastronomie)", 137 | "lon":"13.298201", 138 | "lat":"52.498396", 139 | "type":"POI" 140 | },{ 141 | "name":"Berlin, Active Hotelhellemitte (Hotel)", 142 | "lon":"13.603600", 143 | "lat":"52.537302", 144 | "type":"POI" 145 | },{ 146 | "name":"Berlin, Acud (Kultur und Unterhaltung)", 147 | "lon":"13.400003", 148 | "lat":"52.532897", 149 | "type":"POI" 150 | },{ 151 | "name":"Berlin, Adagio (Gastronomie)", 152 | "lon":"13.398197", 153 | "lat":"52.578203", 154 | "type":"POI" 155 | },{ 156 | "name":"Berlin, Adam (Hotel)", 157 | "lon":"13.302300", 158 | "lat":"52.506397", 159 | "type":"POI" 160 | },{ 161 | "name":"Berlin, Adams (Gastronomie)", 162 | "lon":"13.388497", 163 | "lat":"52.519296", 164 | "type":"POI" 165 | },{ 166 | "name":"Berlin, Adass Jisroel (Öffentliche Einrichtungen)", 167 | "lon":"13.393298", 168 | "lat":"52.527000", 169 | "type":"POI" 170 | },{ 171 | "name":"Berlin, Addis (Gastronomie)", 172 | "lon":"13.387104", 173 | "lat":"52.498001", 174 | "type":"POI" 175 | },{ 176 | "name":"Berlin, Adebar (Gastronomie)", 177 | "lon":"13.405496", 178 | "lat":"52.524798", 179 | "type":"POI" 180 | },{ 181 | "name":"Berlin, Adele (Gastronomie)", 182 | "lon":"13.425101", 183 | "lat":"52.529598", 184 | "type":"POI" 185 | },{ 186 | "name":"Berlin, Adina Apartment Hotel Hauptbahnhof (Hotel)", 187 | "lon":"13.377962", 188 | "lat":"52.528366", 189 | "type":"POI" 190 | },{ 191 | "name":"Berlin, Adlermühle (Gastronomie)", 192 | "lon":"13.394997", 193 | "lat":"52.425197", 194 | "type":"POI" 195 | },{ 196 | "name":"Berlin, Adlon (Hotel)", 197 | "lon":"13.380200", 198 | "lat":"52.516303", 199 | "type":"POI" 200 | },{ 201 | "name":"Berlin, Adlonstube (Gastronomie)", 202 | "lon":"13.380200", 203 | "lat":"52.516303", 204 | "type":"POI" 205 | },{ 206 | "name":"Berlin, Admiralspalast (Kultur und Unterhaltung)", 207 | "lon":"13.388201", 208 | "lat":"52.520600", 209 | "type":"POI" 210 | },{ 211 | "name":"Berlin, Adnan (Gastronomie)", 212 | "lon":"13.317096", 213 | "lat":"52.503700", 214 | "type":"POI" 215 | },{ 216 | "name":"Berlin, Adolf-Glassbrenner-Grundschule (Öffentlich", 217 | "lon":"13.381396", 218 | "lat":"52.491601", 219 | "type":"POI" 220 | },{ 221 | "name":"Berlin, Adolf Reichwein Schule (Öffentliche Einric", 222 | "lon":"13.440401", 223 | "lat":"52.484400", 224 | "type":"POI" 225 | },{ 226 | "name":"Berlin, Adrema (Hotel)", 227 | "lon":"13.329501", 228 | "lat":"52.523297", 229 | "type":"POI" 230 | },{ 231 | "name":"Berlin, Adria (Kultur und Unterhaltung)", 232 | "lon":"13.317896", 233 | "lat":"52.454196", 234 | "type":"POI" 235 | },{ 236 | "name":"Berlin, Aedes (Gastronomie)", 237 | "lon":"13.321896", 238 | "lat":"52.505003", 239 | "type":"POI" 240 | },{ 241 | "name":"Berlin, African Affair (Gastronomie)", 242 | "lon":"13.349898", 243 | "lat":"52.506801", 244 | "type":"POI" 245 | },{ 246 | "name":"Berlin, Agon (Hotel)", 247 | "lon":"13.420697", 248 | "lat":"52.524699", 249 | "type":"POI" 250 | },{ 251 | "name":"Berlin, Agon Opera (Hotel)", 252 | "lon":"13.312404", 253 | "lat":"52.500203", 254 | "type":"POI" 255 | }] 256 | } 257 | } -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var Promise = require('es6-promise').Promise; 5 | 6 | var Fahrplan = require('..'); 7 | var config = require('./config'); 8 | 9 | describe('Fahrplan', function () { 10 | it('is a function', function () { 11 | expect(Fahrplan).to.be.a('function'); 12 | }); 13 | 14 | it('expects an API key as its argument', function () { 15 | expect(function () { Fahrplan() }).to.throw(); 16 | expect(function () { Fahrplan('topsecret') }).not.to.throw(); 17 | }); 18 | 19 | it('returns an object with "station.find", "departure.find" and "arrival.find" methods', function () { 20 | var fahrplan = new Fahrplan('topsecret'); 21 | expect(fahrplan.station.find).to.be.a('function'); 22 | expect(fahrplan.departure.find).to.be.a('function'); 23 | expect(fahrplan.arrival.find).to.be.a('function'); 24 | }); 25 | }); 26 | 27 | describe('fahrplan', function () { 28 | var fahrplan = Fahrplan(config.key); 29 | var berlinId; 30 | 31 | describe('#station.find()', function () { 32 | it('resolves with an object of "stations" and "places"', function (done) { 33 | fahrplan.station.find('Berlin') 34 | .then(function (result) { 35 | expect(result.stations).to.be.an('array'); 36 | expect(result.places).to.be.an('array'); 37 | 38 | expect(result.stations[0].name).to.equal('BERLIN'); 39 | expect(result.stations[0].departure.find).to.be.a('function'); 40 | expect(result.stations[0].arrival.find).to.be.a('function'); 41 | done(); 42 | 43 | berlinId = result.stations[0].id; 44 | }) 45 | .catch(done); 46 | }); 47 | }); 48 | 49 | describe('#station.get()', function () { 50 | it('resolves with the first result of the equivalent `station.find()` query', function (done) { 51 | Promise.all([ 52 | fahrplan.station.find('Berlin Hbf'), 53 | fahrplan.station.get('Berlin Hbf'), 54 | ]) 55 | .then(function (results) { 56 | var find = results[0], get = results[1]; 57 | // Can't use deep.equal because departure.find/arrival.find 58 | // aren't equal 59 | expect(find.stations[0].id).to.equal(get.id); 60 | expect(find.stations[0].name).to.equal(get.name); 61 | expect(find.stations[0].latitude).to.equal(get.latitude); 62 | expect(find.stations[0].longitude).to.equal(get.longitude); 63 | done(); 64 | }) 65 | .catch(done); 66 | }); 67 | 68 | it('resolves with null if no station was found', function (done) { 69 | fahrplan.station.get('') 70 | .then(function (result) { 71 | expect(result).to.be.null; 72 | done(); 73 | }) 74 | .catch(done); 75 | }); 76 | }); 77 | 78 | describe('#departure.find()', function () { 79 | it('resolves with an array of "departures"', function (done) { 80 | fahrplan.departure.find(berlinId) 81 | .then(function (departures) { 82 | expect(departures).to.have.length.above(0); 83 | departures.forEach(function (departure) { 84 | expect(departure).to.contain.keys('name', 'type', 'station', 'departure', 'destination'); 85 | expect(departure.departure).to.be.an.instanceOf(Date); 86 | }); 87 | done(); 88 | }) 89 | .catch(done); 90 | }); 91 | 92 | it('accepts station IDs, station names, station objects, and promises resolving to station objects', function (done) { 93 | Promise.all([ 94 | fahrplan.station.get('B') 95 | .then(function (station) { return fahrplan.departure.find(station.id) }), 96 | fahrplan.departure.find('B'), 97 | fahrplan.station.get('B') 98 | .then(function (station) { return fahrplan.departure.find(station) }), 99 | fahrplan.departure.find(fahrplan.station.get('B')) 100 | ]) 101 | .then(function (results) { 102 | var byName = results[0]; 103 | var byId = results[1]; 104 | var byObject = results[2]; 105 | var byPromise = results[3]; 106 | // Can't use deep.equal because itinerary.get aren't equal 107 | var keys = [ 'name', 'type', 'station', 'departure', 'destination', 'platform' ]; 108 | byName.forEach(function (station, i) { 109 | keys.forEach(function (key) { 110 | expect(byName[i][key]).to.deep.equal(byId[i][key]); 111 | expect(byObject[i][key]).to.deep.equal(byId[i][key]); 112 | expect(byPromise[i][key]).to.deep.equal(byId[i][key]); 113 | }); 114 | }); 115 | done(); 116 | }) 117 | .catch(done); 118 | }); 119 | 120 | it('accepts a date as a second argument', function (done) { 121 | var date = new Date(Date.now() + 24 * 60 * 60 * 1000); 122 | fahrplan.departure.find(berlinId, date) 123 | .then(function (departures) { 124 | expect(departures).to.have.length.above(0); 125 | departures.forEach(function (departure) { 126 | expect(departure.departure.valueOf() >= 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/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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------