diff --git a/docs/changelog.md b/docs/changelog.md index 6b5bc0a1..135c5d70 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,39 @@ # Changelog +## `2.6.0` + +- 5d10d76 journey legs: parse cycle + +## `2.5.3` + +- d676b84 fix parsing for journey leg alternatives ๐Ÿ› + +## `2.5.2` + +- 16e6dd6 departure docs: fix method ๐Ÿ“ +- c60213a DB: tram mode should be `train` ๐Ÿ› + +## `2.5.1` + +- afc0124 fix stopover parsing ๐Ÿ› + ## `2.5.0` - new [Schleswig-Holstein (NAH.SH)](https://de.wikipedia.org/wiki/Nahverkehrsverbund_Schleswig-Holstein) [profile](../p/nahsh) - new [*writing a profile* guide](./writing-a-profile.md) + +## `2.4.2` + +- `parseStopover`: expose canceled arrivals & departures ๐Ÿ› + +## `2.4.1` + +- new [*writing a profile* guide](./writing-a-profile.md) +- `parseMovement`: use `parseStopover` ๐Ÿ› +- `parseStopover`: use `parseStationName` ๐Ÿ› + +## `2.4.0` + +- new [Nahverkehr Sachsen-Anhalt (NASA)](https://de.wikipedia.org/wiki/Nahverkehrsservice_Sachsen-Anhalt)/[INSA](https://insa.de) profile +- new `earlierRef`/`laterRef` feature to query earlier/later journeys (pagination) +- former scheduled date & time for canceled departures & journeys diff --git a/docs/departures.md b/docs/departures.md index 6438b986..1a16535a 100644 --- a/docs/departures.md +++ b/docs/departures.md @@ -44,7 +44,7 @@ const vbbProfile = require('hafas-client/p/vbb') const client = createClient(vbbProfile) // S Charlottenburg -client.journeys('900000024101', {duration: 3}) +client.departures('900000024101', {duration: 3}) .then(console.log) .catch(console.error) ``` diff --git a/docs/journey-leg.md b/docs/journey-leg.md index 95de8c14..f54b0e6e 100644 --- a/docs/journey-leg.md +++ b/docs/journey-leg.md @@ -25,7 +25,8 @@ With `opt`, you can override the default options, which look like this: ```js { when: new Date(), - passedStations: true // return stations on the way? + passedStations: true, // return stations on the way? + polyline: false // return a shape for the leg? } ``` @@ -116,3 +117,5 @@ The response looked like this: passed: [ /* โ€ฆ */ ] } ``` + +If you pass `polyline: true`, the leg will have a `polyline` field, containing an encoded shape. You can use e.g. [`@mapbox/polyline`](https://www.npmjs.com/package/@mapbox/polyline) to decode it. diff --git a/docs/journeys.md b/docs/journeys.md index e101cbf5..1b6d0051 100644 --- a/docs/journeys.md +++ b/docs/journeys.md @@ -60,7 +60,8 @@ With `opt`, you can override the default options, which look like this: express: true, regional: true }, - tickets: false // return tickets? only available with some profiles + tickets: false, // return tickets? only available with some profiles + polylines: false // return a shape for each leg? } ``` @@ -260,3 +261,5 @@ client.journeys(hbf, heinrichHeineStr) departure of last journey 2017-12-17T19:07:00.000+01:00 departure of first (later) journey 2017-12-17T19:19:00.000+01:00 ``` + +If you pass `polylines: true`, each journey leg will have a `polyline` field, containing an encoded shape. You can use e.g. [`@mapbox/polyline`](https://www.npmjs.com/package/@mapbox/polyline) to decode it. diff --git a/docs/writing-a-profile.md b/docs/writing-a-profile.md index d05caebb..8d5e0236 100644 --- a/docs/writing-a-profile.md +++ b/docs/writing-a-profile.md @@ -8,7 +8,7 @@ This guide is about writing such a profile. If you just want to use an already supported endpoint, refer to the [API documentation](readme.md) instead. -*Note*: **If you get stuck, ask for help by [creating an issue](https://github.com/derhuerst/hafas-client/issues/new)!** We want to help people expand the scope of this library. +*Note*: **If you get stuck, ask for help by [creating an issue](https://github.com/public-transport/hafas-client/issues/new)!** We want to help people expand the scope of this library. ## 0. How do the profiles work? @@ -61,7 +61,7 @@ If you pass this profile into `hafas-client`, the `parseLine` method will overri 1. **Get an iOS or Android device and download the "official" app** for the public transport provider that you want to build a profile for. 2. **Configure a [man-in-the-middle HTTP proxy](https://docs.mitmproxy.org/stable/concepts-howmitmproxyworks/)** like [mitmproxy](https://mitmproxy.org). - Configure your device to trust the self-signed SSL certificate, [as outlined in the mitmproxy docs](https://docs.mitmproxy.org/stable/concepts-certificates/). - - *Note*: This method does not work if the app uses [public key pinning](https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning). In this case (the app won't be able to query data), please [create an issue](https://github.com/derhuerst/hafas-client/issues/new), so we can discuss other techniques. + - *Note*: This method does not work if the app uses [public key pinning](https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning). In this case (the app won't be able to query data), please [create an issue](https://github.com/public-transport/hafas-client/issues/new), so we can discuss other techniques. 3. **Record requests of the app.** - [There's a video showing this step](https://stuff.jannisr.de/how-to-record-hafas-requests.mp4). - Make sure to cover all relevant sections of the app, e.g. "journeys", "departures", "live map". Better record more than less; You will regret not having enough information later on. @@ -74,7 +74,7 @@ If you pass this profile into `hafas-client`, the `parseLine` method will overri - **Identify the `locale`.** Basically guess work; Use the date & time formats as an indicator. - **Identify the `timezone`.** This may be tricky, a for example [Deutsche Bahn](https://en.wikipedia.org/wiki/Deutsche_Bahn) returns departures for Moscow as `+01:00` instead of `+03:00`. - **Copy the authentication** and other meta fields, namely `ver`, `ext`, `client` and `lang`. - - You can find these fields in the root of each request JSON. Check [a VBB request](https://gist.github.com/derhuerst/5fa86ed5aec63645e5ae37e23e555886#file-1-http-L13-L22) and [the corresponding VBB profile](https://github.com/derhuerst/hafas-client/blob/6e61097687a37b60d53e767f2711466b80c5142c/p/vbb/index.js#L22-L29) for an example. + - You can find these fields in the root of each request JSON. Check [a VBB request](https://gist.github.com/derhuerst/5fa86ed5aec63645e5ae37e23e555886#file-1-http-L13-L22) and [the corresponding VBB profile](https://github.com/public-transport/hafas-client/blob/6e61097687a37b60d53e767f2711466b80c5142c/p/vbb/index.js#L22-L29) for an example. - Add a function `transformReqBody(body)` to your profile, which assigns them to `body`. - Some profiles have a `checksum` parameter (like [here](https://gist.github.com/derhuerst/2a735268bd82a0a6779633f15dceba33#file-journey-details-1-http-L1)) or two `mic` & `mac` parameters (like [here](https://gist.github.com/derhuerst/5fa86ed5aec63645e5ae37e23e555886#file-1-http-L1)). If you see one of them in your requests, jump to [*Appendix A: checksum, mic, mac*](#appendix-a-checksum-mic-mac). Unfortunately, this is necessary to get the profile working. @@ -160,10 +160,10 @@ You can just query these, as long as you send a formally correct request. ### endpoints using the `checksum` query parameter -`checksum` is a [message authentication code](https://en.wikipedia.org/wiki/Message_authentication_code): `hafas-client` will compute it by [hashing](https://en.wikipedia.org/wiki/Hash_function) the request body and a secret *salt*. **This secret can be read from the config file inside the app bundle.** There is no guide for this yet, so please [open an issue](https://github.com/derhuerst/hafas-client/issues/new) instead. +`checksum` is a [message authentication code](https://en.wikipedia.org/wiki/Message_authentication_code): `hafas-client` will compute it by [hashing](https://en.wikipedia.org/wiki/Hash_function) the request body and a secret *salt*. **This secret can be read from the config file inside the app bundle.** There is no guide for this yet, so please [open an issue](https://github.com/public-transport/hafas-client/issues/new) instead. ### endpoints using the `mic` & `mac` query parameters `mic` is a [message integrity code](https://en.wikipedia.org/wiki/Message_authentication_code), the [hash](https://en.wikipedia.org/wiki/Hash_function) of the request body. -`mac` is a [message authentication code](https://en.wikipedia.org/wiki/Message_authentication_code), the hash of `mic` and a secret *salt*. **This secret can be read from the config file inside the app bundle.** There is no guide for this yet, so please [open an issue](https://github.com/derhuerst/hafas-client/issues/new) instead. +`mac` is a [message authentication code](https://en.wikipedia.org/wiki/Message_authentication_code), the hash of `mic` and a secret *salt*. **This secret can be read from the config file inside the app bundle.** There is no guide for this yet, so please [open an issue](https://github.com/public-transport/hafas-client/issues/new) instead. diff --git a/index.js b/index.js index 7d0482b7..d8cc7734 100644 --- a/index.js +++ b/index.js @@ -93,6 +93,7 @@ const createClient = (profile, request = _request) => { accessibility: 'none', // 'none', 'partial' or 'complete' bike: false, // only bike-friendly journeys tickets: false, // return tickets? + polylines: false // return leg shapes? }, opt) if (opt.via) opt.via = profile.formatLocation(profile, opt.via) opt.when = opt.when || new Date() @@ -113,7 +114,7 @@ const createClient = (profile, request = _request) => { // `CGI_READ_FAILED` if you pass `numF`, the parameter for the number // of results. To circumvent this, we loop here, collecting journeys // until we have enough. - // see https://github.com/derhuerst/hafas-client/pull/23#issuecomment-370246163 + // see https://github.com/public-transport/hafas-client/pull/23#issuecomment-370246163 // todo: check if `numF` is supported again, revert this change const journeys = [] const more = (when, journeysRef) => { @@ -134,7 +135,7 @@ const createClient = (profile, request = _request) => { getPT: true, // todo: what is this? outFrwd: true, // todo: what is this? getIV: false, // todo: walk & bike as alternatives? - getPolyline: false // todo: shape for displaying on a map? + getPolyline: !!opt.polylines } if (profile.journeysNumF) query.numF = opt.results @@ -145,7 +146,13 @@ const createClient = (profile, request = _request) => { }) .then((d) => { if (!Array.isArray(d.outConL)) return [] - const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks) + + let polylines = [] + if (opt.polylines && Array.isArray(d.common.polyL)) { + polylines = d.common.polyL + } + const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks, polylines) + if (!journeys.earlierRef) journeys.earlierRef = d.outCtxScrB let latestDep = -Infinity @@ -271,7 +278,8 @@ const createClient = (profile, request = _request) => { throw new Error('lineName must be a non-empty string.') } opt = Object.assign({ - passedStations: true // return stations on the way? + passedStations: true, // return stations on the way? + polyline: false }, opt) opt.when = opt.when || new Date() @@ -279,13 +287,19 @@ const createClient = (profile, request = _request) => { cfg: {polyEnc: 'GPA'}, meth: 'JourneyDetails', req: { + // todo: getTrainComposition jid: ref, name: lineName, - date: profile.formatDate(profile, opt.when) + date: profile.formatDate(profile, opt.when), + getPolyline: !!opt.polyline } }) .then((d) => { - const parse = profile.parseJourneyLeg(profile, d.locations, d.lines, d.remarks) + let polylines = [] + if (opt.polyline && Array.isArray(d.common.polyL)) { + polylines = d.common.polyL + } + const parse = profile.parseJourneyLeg(profile, d.locations, d.lines, d.remarks, polylines) const leg = { // pretend the leg is contained in a journey type: 'JNY', diff --git a/lib/request.js b/lib/request.js index aaca1558..084fe26b 100644 --- a/lib/request.js +++ b/lib/request.js @@ -20,7 +20,7 @@ const request = (profile, data) => { headers: { 'Content-Type': 'application/json', 'Accept-Encoding': 'gzip, deflate', - 'user-agent': 'https://github.com/derhuerst/hafas-client' + 'user-agent': 'https://github.com/public-transport/hafas-client' }, query: {} }) diff --git a/p/vbb/example.js b/p/vbb/example.js index 335d83ff..82563af6 100644 --- a/p/vbb/example.js +++ b/p/vbb/example.js @@ -6,7 +6,7 @@ const vbbProfile = require('.') const client = createClient(vbbProfile) // Hauptbahnhof to Charlottenburg -client.journeys('900000003201', '900000024101', {results: 1}) +client.journeys('900000003201', '900000024101', {results: 1, polylines: true}) // client.departures('900000013102', {duration: 1}) // client.locations('Alexanderplatz', {results: 2}) // client.location('900000042101') // Spichernstr @@ -18,6 +18,10 @@ client.journeys('900000003201', '900000024101', {results: 1}) // east: 13.41709 // }, {results: 10}) +// .then(([journey]) => { +// const leg = journey.legs[0] +// return client.journeyLeg(leg.id, leg.line.name, {polyline: true}) +// }) .then((data) => { console.log(require('util').inspect(data, {depth: null})) }) diff --git a/p/vbb/index.js b/p/vbb/index.js index 8a3539cd..1366e49f 100644 --- a/p/vbb/index.js +++ b/p/vbb/index.js @@ -56,8 +56,8 @@ const parseLocation = (profile, l, lines) => { return res } -const createParseJourney = (profile, stations, lines, remarks) => { - const parseJourney = _createParseJourney(profile, stations, lines, remarks) +const createParseJourney = (profile, stations, lines, remarks, polylines) => { + const parseJourney = _createParseJourney(profile, stations, lines, remarks, polylines) const parseJourneyWithTickets = (j) => { const res = parseJourney(j) diff --git a/package.json b/package.json index 9c305030..9045e677 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hafas-client", "description": "JavaScript client for HAFAS public transport APIs.", - "version": "2.5.0", + "version": "2.6.0", "main": "index.js", "files": [ "index.js", @@ -16,9 +16,9 @@ "contributors": [ "Julius Tens " ], - "homepage": "https://github.com/derhuerst/hafas-client", - "repository": "derhuerst/hafas-client", - "bugs": "https://github.com/derhuerst/hafas-client/issues", + "homepage": "https://github.com/public-transport/hafas-client", + "repository": "public-transport/hafas-client", + "bugs": "https://github.com/public-transport/hafas-client/issues", "license": "ISC", "keywords": [ "hafas", diff --git a/parse/departure.js b/parse/departure.js index 3868c988..158eb1ea 100644 --- a/parse/departure.js +++ b/parse/departure.js @@ -1,10 +1,10 @@ 'use strict' -// todos from derhuerst/hafas-client#2 +// todos from public-transport/hafas-client#2 // - stdStop.dPlatfS, stdStop.dPlatfR // todo: what is d.jny.dirFlg? // todo: d.stbStop.dProgType -// todo: d.freq, d.freq.jnyL, see https://github.com/derhuerst/hafas-client/blob/9203ed1481f08baacca41ac5e3c19bf022f01b0b/parse.js#L115 +// todo: d.freq, d.freq.jnyL, see https://github.com/public-transport/hafas-client/blob/9203ed1481f08baacca41ac5e3c19bf022f01b0b/parse.js#L115 const createParseDeparture = (profile, stations, lines, remarks) => { const findRemark = rm => remarks[parseInt(rm.remX)] || null diff --git a/parse/journey-leg.js b/parse/journey-leg.js index 30477270..65032de2 100644 --- a/parse/journey-leg.js +++ b/parse/journey-leg.js @@ -4,7 +4,7 @@ const parseDateTime = require('./date-time') const clone = obj => Object.assign({}, obj) -const createParseJourneyLeg = (profile, stations, lines, remarks) => { +const createParseJourneyLeg = (profile, stations, lines, remarks, polylines) => { // todo: finish parse/remark.js first const applyRemark = (j, rm) => {} @@ -34,6 +34,13 @@ const createParseJourneyLeg = (profile, stations, lines, remarks) => { res.arrivalDelay = Math.round((realtime - planned) / 1000) } + if (pt.jny && pt.jny.polyG) { + let p = pt.jny.polyG.polyXL + p = p && polylines[p[0]] + // todo: there can be >1 polyline + res.polyline = p && p.crdEncYX || null + } + if (pt.type === 'WALK') { res.mode = 'walking' res.public = true @@ -56,18 +63,25 @@ const createParseJourneyLeg = (profile, stations, lines, remarks) => { for (let remark of pt.jny.remL) applyRemark(j, remark) } - if (pt.jny.freq && pt.jny.freq.jnyL) { + const freq = pt.jny.freq || {} + if (freq.minC && freq.maxC) { + // todo: what is freq.numC? + res.cycle = { + min: freq.minC * 60, + max: freq.maxC * 60 + } + } + if (freq.jnyL) { const parseAlternative = (a) => { - const t = a.stopL[0].dTimeS || a.stopL[0].dTimeR + const t = a.stopL[0].dTimeR || a.stopL[0].dTimeS const when = profile.parseDateTime(profile, j.date, t) + // todo: expose a.stopL[0] return { line: lines[parseInt(a.prodX)] || null, when: when.toISO() } } - res.alternatives = pt.jny.freq.jnyL - .filter(a => a.stopL[0].locX === pt.dep.locX) - .map(parseAlternative) + res.alternatives = freq.jnyL.map(parseAlternative) } } diff --git a/parse/journey.js b/parse/journey.js index 09155696..24aaf8c8 100644 --- a/parse/journey.js +++ b/parse/journey.js @@ -1,11 +1,9 @@ 'use strict' -const createParseJourneyLeg = require('./journey-leg') - const clone = obj => Object.assign({}, obj) -const createParseJourney = (profile, stations, lines, remarks) => { - const parseLeg = createParseJourneyLeg(profile, stations, lines, remarks) +const createParseJourney = (profile, stations, lines, remarks, polylines) => { + const parseLeg = profile.parseJourneyLeg(profile, stations, lines, remarks, polylines) // todo: c.sDays // todo: c.conSubscr diff --git a/parse/stopover.js b/parse/stopover.js index 86d12726..8315c814 100644 --- a/parse/stopover.js +++ b/parse/stopover.js @@ -28,12 +28,12 @@ const createParseStopover = (profile, stations, lines, remarks, date) => { Object.defineProperty(res, 'canceled', {value: true}) if (st.aCncl) { res.arrival = res.arrivalDelay = null - const arr = profile.parseDateTime(profile, d.date, st.aTimeS) + const arr = profile.parseDateTime(profile, date, st.aTimeS) res.formerScheduledArrival = arr.toISO() } if (st.dCncl) { res.departure = res.departureDelay = null - const arr = profile.parseDateTime(profile, d.date, st.dTimeS) + const arr = profile.parseDateTime(profile, date, st.dTimeS) res.formerScheduledDeparture = arr.toISO() } } diff --git a/readme.md b/readme.md index d12d8df5..d5b042ef 100644 --- a/readme.md +++ b/readme.md @@ -7,12 +7,12 @@ HAFAS endpoint | wrapper library | docs | example code | source code [Deutsche Bahn (DB)](https://en.wikipedia.org/wiki/Deutsche_Bahn) | [`db-hafas`](https://github.com/derhuerst/db-hafas) | [docs](p/db/readme.md) | [example code](p/db/example.js) | [src](p/db/index.js) [Berlin & Brandenburg public transport (VBB)](https://en.wikipedia.org/wiki/Verkehrsverbund_Berlin-Brandenburg) | [`vbb-hafas`](https://github.com/derhuerst/vbb-hafas) | [docs](p/vbb/readme.md) | [example code](p/vbb/example.js) | [src](p/vbb/index.js) [ร–sterreichische Bundesbahnen (ร–BB)](https://en.wikipedia.org/wiki/Austrian_Federal_Railways) | [`oebb-hafas`](https://github.com/juliuste/oebb-hafas) | [docs](p/oebb/readme.md) | [example code](p/oebb/example.js) | [src](p/oebb/index.js) -[Nahverkehr Sachsen-Anhalt (NASA)](https://de.wikipedia.org/wiki/Nahverkehrsservice_Sachsen-Anhalt)/[INSA](https://insa.de) | โ€“ | [docs](p/insa/readme.md) | [example code](p/insa/example.js) | [src](p/insa/index.js) +[Nahverkehr Sachsen-Anhalt (NASA)](https://de.wikipedia.org/wiki/Nahverkehrsservice_Sachsen-Anhalt)/[INSA](https://insa.de) | [`insa-hafas`](https://github.com/derhuerst/insa-hafas) | [docs](p/insa/readme.md) | [example code](p/insa/example.js) | [src](p/insa/index.js) [Nahverkehrsverbund Schleswig-Holstein (NAH.SH)](https://de.wikipedia.org/wiki/Nahverkehrsverbund_Schleswig-Holstein) | [`nahsh-hafas`](https://github.com/juliuste/nahsh-hafas) | [docs](p/nahsh/readme.md) | [example code](p/nahsh/example.js) | [src](p/nahsh/index.js) [![npm version](https://img.shields.io/npm/v/hafas-client.svg)](https://www.npmjs.com/package/hafas-client) -[![build status](https://img.shields.io/travis/derhuerst/hafas-client.svg)](https://travis-ci.org/derhuerst/hafas-client) -![ISC-licensed](https://img.shields.io/github/license/derhuerst/hafas-client.svg) +[![build status](https://img.shields.io/travis/public-transport/hafas-client.svg)](https://travis-ci.org/public-transport/hafas-client) +![ISC-licensed](https://img.shields.io/github/license/public-transport/hafas-client.svg) [![chat on gitter](https://badges.gitter.im/derhuerst.svg)](https://gitter.im/derhuerst) [![support me on Patreon](https://img.shields.io/badge/support%20me-on%20patreon-fa7664.svg)](https://patreon.com/derhuerst) @@ -181,4 +181,4 @@ The returned [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript ## Contributing -If you **have a question**, **found a bug** or want to **propose a feature**, have a look at [the issues page](https://github.com/derhuerst/hafas-client/issues). +If you **have a question**, **found a bug** or want to **propose a feature**, have a look at [the issues page](https://github.com/public-transport/hafas-client/issues). diff --git a/test/oebb.js b/test/oebb.js index feae6c65..2c7c9623 100644 --- a/test/oebb.js +++ b/test/oebb.js @@ -95,7 +95,7 @@ const assertValidPrice = (t, p) => { } // todo: fix this upstream -// see https://github.com/derhuerst/hafas-client/blob/c6e558be217667f1bcdac4a605898eb75ea80374/p/oebb/products.js#L71 +// see https://github.com/public-transport/hafas-client/blob/c6e558be217667f1bcdac4a605898eb75ea80374/p/oebb/products.js#L71 const assertValidLine = (t, l) => { // with optional mode const validators = Object.assign({}, validateFptf.defaultValidators, { line: validateLineWithoutMode diff --git a/test/validate-line-without-mode.js b/test/validate-line-without-mode.js index 71068e32..8f916348 100644 --- a/test/validate-line-without-mode.js +++ b/test/validate-line-without-mode.js @@ -19,7 +19,7 @@ const validateLineWithoutMode = (validate, line, name) => { a.ok(line.name.length > 0, name + '.name can\'t be empty') // skipping line validation here - // see https://github.com/derhuerst/hafas-client/issues/8#issuecomment-355839965 + // see https://github.com/public-transport/hafas-client/issues/8#issuecomment-355839965 if (is.undefined(line.mode) || is.null(line.mode)) { console.error(`ร–BB: Missing \`mode\` for line ${line.name} (at ${name}).`) }