mirror of
				https://github.com/public-transport/db-vendo-client.git
				synced 2025-10-30 23:56:31 +02:00 
			
		
		
		
	Merge branch 'next' into format-location-err-msg
This commit is contained in:
		
						commit
						bebb975584
					
				
					 23 changed files with 274 additions and 60 deletions
				
			
		|  | @ -1,6 +1,45 @@ | |||
| # Changelog | ||||
| 
 | ||||
| ## `2.7.0` | ||||
| 
 | ||||
| - `journeys()`: `polylines` option | ||||
| - `journeyLeg()`: `polyline` option | ||||
| - `radar()`: `polylines` option | ||||
| 
 | ||||
| ## `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 | ||||
|  |  | |||
|  | @ -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) | ||||
| ``` | ||||
|  |  | |||
|  | @ -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,67 @@ The response looked like this: | |||
| 	passed: [ /* … */ ] | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### `polyline` option | ||||
| 
 | ||||
| If you pass `polyline: true`, the leg will have a `polyline` field, containing a [GeoJSON](http://geojson.org) [`FeatureCollection`](https://tools.ietf.org/html/rfc7946#section-3.3) of [`Point`s](https://tools.ietf.org/html/rfc7946#appendix-A.1). Every `Point` next to a station will have `properties` containing the station's metadata. | ||||
| 
 | ||||
| We'll look at an example for *U6* from *Alt-Mariendorf* to *Alt-Tegel*, taken from the [VBB profile](../p/vbb): | ||||
| 
 | ||||
| ```js | ||||
| { | ||||
| 	type: 'FeatureCollection', | ||||
| 	features: [ | ||||
| 		{ | ||||
| 			type: 'Feature', | ||||
| 			properties: { | ||||
| 				type: 'station', | ||||
| 				id: '900000070301', | ||||
| 				name: 'U Alt-Mariendorf', | ||||
| 				/* … */ | ||||
| 			}, | ||||
| 			geometry: { | ||||
| 				type: 'Point', | ||||
| 				coordinates: [13.3875, 52.43993] // longitude, latitude | ||||
| 			} | ||||
| 		}, | ||||
| 		/* … */ | ||||
| 		{ | ||||
| 			type: 'Feature', | ||||
| 			properties: { | ||||
| 				type: 'station', | ||||
| 				id: '900000017101', | ||||
| 				name: 'U Mehringdamm', | ||||
| 				/* … */ | ||||
| 			}, | ||||
| 			geometry: { | ||||
| 				type: 'Point', | ||||
| 				coordinates: [13.38892, 52.49448] // longitude, latitude | ||||
| 			} | ||||
| 		}, | ||||
| 		/* … */ | ||||
| 		{ | ||||
| 			// intermediate point, without associated station | ||||
| 			type: 'Feature', | ||||
| 			properties: {}, | ||||
| 			geometry: { | ||||
| 				type: 'Point', | ||||
| 				coordinates: [13.28599, 52.58742] // longitude, latitude | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
| 			type: 'Feature', | ||||
| 			properties: { | ||||
| 				type: 'station', | ||||
| 				id: '900000089301', | ||||
| 				name: 'U Alt-Tegel', | ||||
| 				/* … */ | ||||
| 			}, | ||||
| 			geometry: { | ||||
| 				type: 'Point', | ||||
| 				coordinates: [13.28406, 52.58915] // longitude, latitude | ||||
| 			} | ||||
| 		} | ||||
| 	] | ||||
| } | ||||
| ``` | ||||
|  |  | |||
|  | @ -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. Refer to [the section in the `journeyLeg()` docs](journey-leg.md#polyline-option) for details. | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ With `opt`, you can override the default options, which look like this: | |||
| 	results: 256, // maximum number of vehicles | ||||
| 	duration: 30, // compute frames for the next n seconds | ||||
| 	frames: 3, // nr of frames to compute | ||||
| 	polylines: false // return a track shape for each vehicle? | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
|  | @ -161,3 +162,5 @@ The response may look like this: | |||
| 	} ] | ||||
| }, /* … */ ] | ||||
| ``` | ||||
| 
 | ||||
| If you pass `polylines: true`, each journey leg will have a `polyline` field, as documented in [the corresponding section in the `journeyLeg()` docs](journey-leg.md#polyline-option), with the exception that station info is missing. | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
							
								
								
									
										40
									
								
								index.js
									
										
									
									
									
								
							
							
						
						
									
										40
									
								
								index.js
									
										
									
									
									
								
							|  | @ -31,7 +31,8 @@ const createClient = (profile, request = _request) => { | |||
| 			direction: null, // only show departures heading to this station
 | ||||
| 			duration:  10 // show departures for the next n minutes
 | ||||
| 		}, opt) | ||||
| 		opt.when = opt.when || new Date() | ||||
| 		opt.when = new Date(opt.when || Date.now()) | ||||
| 		if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid') | ||||
| 		const products = profile.formatProductsFilter(opt.products || {}) | ||||
| 
 | ||||
| 		const dir = opt.direction ? profile.formatStation(opt.direction) : null | ||||
|  | @ -93,9 +94,11 @@ 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.via') | ||||
| 		opt.when = opt.when || new Date() | ||||
| 		opt.when = new Date(opt.when || Date.now()) | ||||
| 		if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid') | ||||
| 
 | ||||
| 		const filters = [ | ||||
| 			profile.formatProductsFilter(opt.products || {}) | ||||
|  | @ -113,7 +116,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 +137,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 +148,10 @@ const createClient = (profile, request = _request) => { | |||
| 			}) | ||||
| 			.then((d) => { | ||||
| 				if (!Array.isArray(d.outConL)) return [] | ||||
| 				const parse = profile.parseJourney(profile, d.locations, d.lines, d.remarks) | ||||
| 
 | ||||
| 				const polylines = opt.polyline && 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,21 +277,26 @@ 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() | ||||
| 		opt.when = new Date(opt.when || Date.now()) | ||||
| 		if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid') | ||||
| 
 | ||||
| 		return request(profile, { | ||||
| 			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) | ||||
| 			const polylines = opt.polyline && 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', | ||||
|  | @ -302,14 +313,18 @@ const createClient = (profile, request = _request) => { | |||
| 		if ('number' !== typeof west) throw new Error('west must be a number.') | ||||
| 		if ('number' !== typeof south) throw new Error('south must be a number.') | ||||
| 		if ('number' !== typeof east) throw new Error('east must be a number.') | ||||
| 		if (north <= south) throw new Error('north must be larger than south.') | ||||
| 		if (east <= west) throw new Error('east must be larger than west.') | ||||
| 
 | ||||
| 		opt = Object.assign({ | ||||
| 			results: 256, // maximum number of vehicles
 | ||||
| 			duration: 30, // compute frames for the next n seconds
 | ||||
| 			frames: 3, // nr of frames to compute
 | ||||
| 			products: null // optionally an object of booleans
 | ||||
| 			products: null, // optionally an object of booleans
 | ||||
| 			polylines: false // return a track shape for each vehicle?
 | ||||
| 		}, opt || {}) | ||||
| 		opt.when = opt.when || new Date() | ||||
| 		opt.when = new Date(opt.when || Date.now()) | ||||
| 		if (Number.isNaN(+opt.when)) throw new Error('opt.when is invalid') | ||||
| 
 | ||||
| 		const durationPerStep = opt.duration / Math.max(opt.frames, 1) * 1000 | ||||
| 		return request(profile, { | ||||
|  | @ -333,7 +348,8 @@ const createClient = (profile, request = _request) => { | |||
| 		.then((d) => { | ||||
| 			if (!Array.isArray(d.jnyL)) return [] | ||||
| 
 | ||||
| 			const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks) | ||||
| 			const polylines = opt.polyline && d.common.polyL || [] | ||||
| 			const parse = profile.parseMovement(profile, d.locations, d.lines, d.remarks, polylines) | ||||
| 			return d.jnyL.map(parse) | ||||
| 		}) | ||||
| 	} | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ const parseJourneyLeg = require('../parse/journey-leg') | |||
| const parseJourney = require('../parse/journey') | ||||
| const parseLine = require('../parse/line') | ||||
| const parseLocation = require('../parse/location') | ||||
| const parsePolyline = require('../parse/polyline') | ||||
| const parseMovement = require('../parse/movement') | ||||
| const parseNearby = require('../parse/nearby') | ||||
| const parseOperator = require('../parse/operator') | ||||
|  | @ -42,6 +43,7 @@ const defaultProfile = { | |||
| 	parseLine, | ||||
| 	parseStationName: id, | ||||
| 	parseLocation, | ||||
| 	parsePolyline, | ||||
| 	parseMovement, | ||||
| 	parseNearby, | ||||
| 	parseOperator, | ||||
|  |  | |||
|  | @ -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: {} | ||||
| 	}) | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ const types = { | |||
| 	parseLine: 'function', | ||||
| 	parseStationName: 'function', | ||||
| 	parseLocation: 'function', | ||||
| 	parsePolyline: 'function', | ||||
| 	parseMovement: 'function', | ||||
| 	parseNearby: 'function', | ||||
| 	parseOperator: 'function', | ||||
|  |  | |||
|  | @ -68,7 +68,7 @@ module.exports = [ | |||
| 	}, | ||||
| 	{ | ||||
| 		id: 'tram', | ||||
| 		mode: 'tram', | ||||
| 		mode: 'train', | ||||
| 		bitmasks: [256], | ||||
| 		name: 'Tram', | ||||
| 		short: 'T', | ||||
|  |  | |||
|  | @ -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})) | ||||
| }) | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
							
								
								
									
										10
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
										
									
									
									
								
							|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	"name": "hafas-client", | ||||
| 	"description": "JavaScript client for HAFAS public transport APIs.", | ||||
| 	"version": "2.5.0", | ||||
| 	"version": "2.7.2", | ||||
| 	"main": "index.js", | ||||
| 	"files": [ | ||||
| 		"index.js", | ||||
|  | @ -16,9 +16,9 @@ | |||
| 	"contributors": [ | ||||
| 		"Julius Tens <mail@juliustens.eu>" | ||||
| 	], | ||||
| 	"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", | ||||
|  | @ -32,8 +32,10 @@ | |||
| 		"node": ">=6" | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"@mapbox/polyline": "^1.0.0", | ||||
| 		"capture-stack-trace": "^1.0.0", | ||||
| 		"fetch-ponyfill": "^6.0.0", | ||||
| 		"gps-distance": "0.0.4", | ||||
| 		"lodash": "^4.17.5", | ||||
| 		"luxon": "^0.5.8", | ||||
| 		"p-throttle": "^1.1.0", | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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,14 @@ 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 = Array.isArray(p) && polylines[p[0]] | ||||
| 			// todo: there can be >1 polyline
 | ||||
| 			const parse = profile.parsePolyline(stations) | ||||
| 			res.polyline = p && parse(p) || null | ||||
| 		} | ||||
| 
 | ||||
| 		if (pt.type === 'WALK') { | ||||
| 			res.mode = 'walking' | ||||
| 			res.public = true | ||||
|  | @ -41,7 +49,7 @@ const createParseJourneyLeg = (profile, stations, lines, remarks) => { | |||
| 			// todo: pull `public` value from `profile.products`
 | ||||
| 			res.id = pt.jny.jid | ||||
| 			res.line = lines[parseInt(pt.jny.prodX)] || null | ||||
| 			res.direction = profile.parseStationName(pt.jny.dirTxt) | ||||
| 			res.direction = profile.parseStationName(pt.jny.dirTxt) || null | ||||
| 
 | ||||
| 			if (pt.dep.dPlatfS) res.departurePlatform = pt.dep.dPlatfS | ||||
| 			if (pt.arr.aPlatfS) res.arrivalPlatform = pt.arr.aPlatfS | ||||
|  | @ -56,18 +64,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) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -1,18 +1,18 @@ | |||
| 'use strict' | ||||
| 
 | ||||
| const createParseMovement = (profile, locations, lines, remarks) => { | ||||
| const createParseMovement = (profile, locations, lines, remarks, polylines = []) => { | ||||
| 	// todo: what is m.dirGeo? maybe the speed?
 | ||||
| 	// todo: what is m.stopL?
 | ||||
| 	// todo: what is m.proc? wut?
 | ||||
| 	// todo: what is m.pos?
 | ||||
| 	// todo: what is m.ani.dirGeo[n]? maybe the speed?
 | ||||
| 	// todo: what is m.ani.proc[n]? wut?
 | ||||
| 	// todo: how does m.ani.poly work?
 | ||||
| 	const parseMovement = (m) => { | ||||
| 		const pStopover = profile.parseStopover(profile, locations, lines, remarks, m.date) | ||||
| 
 | ||||
| 		const res = { | ||||
| 			direction: profile.parseStationName(m.dirTxt), | ||||
| 			journeyId: m.jid || null, | ||||
| 			trip: m.jid && +m.jid.split('|')[1] || null, // todo: this seems brittle
 | ||||
| 			line: lines[m.prodX] || null, | ||||
| 			location: m.pos ? { | ||||
|  | @ -24,13 +24,26 @@ const createParseMovement = (profile, locations, lines, remarks) => { | |||
| 			frames: [] | ||||
| 		} | ||||
| 
 | ||||
| 		if (m.ani && Array.isArray(m.ani.mSec)) { | ||||
| 			for (let i = 0; i < m.ani.mSec.length; i++) { | ||||
| 				res.frames.push({ | ||||
| 					origin: locations[m.ani.fLocX[i]] || null, | ||||
| 					destination: locations[m.ani.tLocX[i]] || null, | ||||
| 					t: m.ani.mSec[i] | ||||
| 				}) | ||||
| 		if (m.ani) { | ||||
| 			if (Array.isArray(m.ani.mSec)) { | ||||
| 				for (let i = 0; i < m.ani.mSec.length; i++) { | ||||
| 					res.frames.push({ | ||||
| 						origin: locations[m.ani.fLocX[i]] || null, | ||||
| 						destination: locations[m.ani.tLocX[i]] || null, | ||||
| 						t: m.ani.mSec[i] | ||||
| 					}) | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			if (m.ani.poly) { | ||||
| 				const parse = profile.parsePolyline(locations) | ||||
| 				res.polyline = parse(m.ani.poly) | ||||
| 			} else if (m.ani.polyG) { | ||||
| 				let p = m.ani.polyG.polyXL | ||||
| 				p = Array.isArray(p) && polylines[p[0]] | ||||
| 				// todo: there can be >1 polyline
 | ||||
| 				const parse = profile.parsePolyline(locations) | ||||
| 				res.polyline = p && parse(p) || null | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										53
									
								
								parse/polyline.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								parse/polyline.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | |||
| 'use strict' | ||||
| 
 | ||||
| const {toGeoJSON} = require('@mapbox/polyline') | ||||
| const distance = require('gps-distance') | ||||
| 
 | ||||
| const createParsePolyline = (locations) => { | ||||
| 	// todo: what is p.delta?
 | ||||
| 	// todo: what is p.type?
 | ||||
| 	// todo: what is p.crdEncS?
 | ||||
| 	// todo: what is p.crdEncF?
 | ||||
| 	const parsePolyline = (p) => { | ||||
| 		const shape = toGeoJSON(p.crdEncYX) | ||||
| 		if (shape.coordinates.length === 0) return null | ||||
| 
 | ||||
| 		const res = shape.coordinates.map(crd => ({ | ||||
| 			type: 'Feature', | ||||
| 			properties: {}, | ||||
| 			geometry: { | ||||
| 				type: 'Point', | ||||
| 				coordinates: crd | ||||
| 			} | ||||
| 		})) | ||||
| 
 | ||||
| 		if (Array.isArray(p.ppLocRefL)) { | ||||
| 			for (let ref of p.ppLocRefL) { | ||||
| 				const p = res[ref.ppIdx] | ||||
| 				const loc = locations[ref.locX] | ||||
| 				if (p && loc) p.properties = loc | ||||
| 			} | ||||
| 
 | ||||
| 			// Often there is one more point right next to each point at a station.
 | ||||
| 			// We filter them here if they are < 5m from each other.
 | ||||
| 			for (let i = 1; i < res.length; i++) { | ||||
| 				const p1 = res[i - 1].geometry.coordinates | ||||
| 				const p2 = res[i].geometry.coordinates | ||||
| 				const d = distance(p1[1], p1[0], p2[1], p2[0]) | ||||
| 				if (d >= .005) continue | ||||
| 				const l1 = Object.keys(res[i - 1].properties).length | ||||
| 				const l2 = Object.keys(res[i].properties).length | ||||
| 				if (l1 === 0 && l2 > 0) res.splice(i - 1, 1) | ||||
| 				else if (l2 === 0 && l1 > 0) res.splice(i, 1) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return { | ||||
| 			type: 'FeatureCollection', | ||||
| 			features: res | ||||
| 		} | ||||
| 	} | ||||
| 	return parsePolyline | ||||
| } | ||||
| 
 | ||||
| module.exports = createParsePolyline | ||||
|  | @ -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() | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
							
								
								
									
										10
									
								
								readme.md
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								readme.md
									
										
									
									
									
								
							|  | @ -7,13 +7,13 @@ 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) | ||||
| 
 | ||||
| [](https://www.npmjs.com/package/hafas-client) | ||||
| [](https://travis-ci.org/derhuerst/hafas-client) | ||||
|  | ||||
| [](https://gitter.im/derhuerst) | ||||
| [](https://travis-ci.org/public-transport/hafas-client) | ||||
|  | ||||
| [](https://gitter.im/public-transport/Lobby) | ||||
| [](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). | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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}).`) | ||||
| 	} | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue