├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .prettierrc.json ├── LICENSE.md ├── README.md ├── docs ├── PROVIDERS.md ├── README.md └── example.ts ├── package-lock.json ├── package.json ├── src ├── RequestAndParse.ts ├── index.ts ├── trias │ ├── TRIASDeparturesHandler.ts │ ├── TRIASJourneysHandler.ts │ └── TRIASStopsHandler.ts ├── types │ ├── fptf.ts │ ├── options.ts │ └── results.ts └── xml │ ├── TRIAS_LIR_NAME.ts │ ├── TRIAS_LIR_POS.ts │ ├── TRIAS_SER.ts │ └── TRIAS_TR.ts ├── test ├── client.test.js └── parse.test.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [**.{ts, json}] 11 | indent_style = spaces 12 | indent_size = 4 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { 5 | "node": true 6 | }, 7 | "plugins": [ 8 | "@typescript-eslint", 9 | "jest" 10 | ], 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:jest/recommended", 15 | "prettier" 16 | ], 17 | "rules": { 18 | "@typescript-eslint/no-unused-vars": "off" 19 | }, 20 | "ignorePatterns": [ 21 | "lib" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | release: 4 | types: 5 | - published 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: 10 14 | - run: npm install 15 | - run: npm run test # Test again to make sure no failing builds get published. Implicitly builds the package. 16 | env: 17 | TEST_CREDENTIALS: ${{ secrets.TEST_CREDENTIALS }} 18 | - uses: JS-DevTools/npm-publish@v1 19 | with: 20 | token: ${{ secrets.NPM_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test and lint 2 | on: 3 | push: 4 | branches: 5 | - "*" 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: 10 14 | - run: npm install 15 | - run: npm run test 16 | env: 17 | TEST_CREDENTIALS: ${{ secrets.TEST_CREDENTIALS }} 18 | lint: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v1 22 | - uses: actions/setup-node@v1 23 | with: 24 | node-version: 10 25 | - run: npm install 26 | - run: npm run lint 27 | 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | test-credentials.json -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 200, 3 | "tabWidth": 4, 4 | "trailingComma": "all", 5 | "singleQuote": false 6 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Andary 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TRIAS Client 2 | 3 | **A lean Node.js client for TRIAS APIs. 🚀** 4 | 5 | This client aims to be an easy to use and lightweight implementation for the public transport TRIAS specification. It achieves that by providing only a small subset of the capabilities of TRIAS and utilizing the [Friendly Public Transport Format](https://github.com/public-transport/friendly-public-transport-format). 6 | 7 | `trias-client` currently implements TRIAS v1.2 and only supports following basic functionalities: 8 | - Searching for stops (either using a name or coordinates). 9 | - Fetching departures for a stop. 10 | - Reading current ticker news for a stop. 11 | - Navigating from stop to stop. 12 | 13 | ## Usage 14 | 15 | > Please note that you will need an API endpoint and an API key or requestor reference key issued by a data provider. Check the [providers list](docs/PROVIDERS.md) for open APIs. 16 | 17 | Install the package: 18 | 19 | ```shell 20 | npm install trias-client 21 | ``` 22 | 23 | Following script creates a client instance, searches for a station and fetches the departures for the first result. Please refer to the [documentation](docs/README.md) for more information. 24 | 25 | ```javascript 26 | const trias = require("trias-client"); 27 | 28 | var client = trias.getClient({ 29 | url: "place the url of the TRIAS API here", 30 | requestorRef: "place your requestor ref here" 31 | }); 32 | 33 | var stopsResult = await client.getStops({ 34 | name: "bismarckplatz" 35 | }); 36 | 37 | var departuresResult = await client.getDepartures({ 38 | id: stopsResult.stops[0].id 39 | }); 40 | ``` 41 | 42 | There's also an [example script](docs/example.ts) available that demonstrates how to use `trias-client`. 43 | 44 | ## What is TRIAS? 45 | 46 | TRIAS stands for "**T**ravellor **R**ealtime **I**nformation and **A**dvisory **S**tandard", has been developed in scope of the research and standardisation project for public transport "IP-KOM-ÖV" and was then introduced in 2014 as a standardized specification by the VDV ([Verband Deutscher Verkehrsunternehmen](https://de.wikipedia.org/wiki/Verband_Deutscher_Verkehrsunternehmen)). TRIAS offers a wide-range list of functionalities, including station / location search, realtime departures, navigation, ticket price calculation, malfunction reportings, and so on. [Here](docs/PROVIDERS.md) is a list of all public transport providers that provide a TRIAS API. 47 | 48 | ## Why TRIAS? 49 | 50 | Compared to [HAFAS](https://github.com/public-transport/hafas-client), TRIAS isn't that widely distributed. But it's a step in the right direction as it allows for some kind of standardization in the jungle of Public Transport APIs. Unfortunately, many of the data providers still build their own proprietary APIs. 51 | 52 | You might wonder why this even matters if you can just continue to use the existing HAFAS interfaces. The biggest difference is that these HAFAS interfaces are not supposed to be used by the public and public transport providers might even prohibit to use them (you can read more about that [here](https://github.com/public-transport/transport.rest/issues/4)). So if you want to develop and publish a project that uses public transport data, you might want to have some kind of agreement with the data provider, that reduces operational and legal risk for both you and the provider. 53 | 54 | And this is where TRIAS becomes relevant, as the APIs built on it are public (not open, as they still require authentication, but public). And while some providers are a bit more strict regarding the use and display of the data, in general all of the APIs have fair terms of use and come with realistic usage quotas. 55 | 56 | ## Related resources 57 | 58 | `trias-client` was originally developed in scope of Abfahrt, a public transport companion (retired as of December 2022) that integrates multiple ÖPNV providers in just one app. You can take a look over there to see this client in action. 59 | 60 | Dou you want to develop your own TRIAS client? Here are some resources: 61 | - [VDV 431-2 EKAP-Schnittstellenbeschreibung (german)](https://www.vdv.de/ip-kom-oev.aspx) 62 | - [VDV TRIAS XML specification](https://github.com/VDVde/TRIAS) 63 | - [TRIAS implementation example in PHP](https://www.vrn.de/opendata/node/118) 64 | - [TRIAS request examples (german)](https://www.verbundlinie.at/fahrplan/rund-um-den-fahrplan/link-zum-fahrplan) 65 | -------------------------------------------------------------------------------- /docs/PROVIDERS.md: -------------------------------------------------------------------------------- 1 | # List of TRIAS Providers 2 | 3 | This is a list of public transport providers that provide a TRIAS API. Be aware that: 4 | - Most of them **do not offer open APIs**, but require you to sign up with them. 5 | - Not all providers offer the same functionalities. The TRIAS specification is *enormous* and most providers only implemented the basic stuff. 6 | - All providers prohibit automated mass requests and enforce some other legalse, e.g. legal disclaimers in your app. Make sure to check their usage agreements. 7 | 8 | 9 | ### [Bayerische Eisenbahngesellschaft (DEFAS)](https://www.bayern-fahrplan.de/de/faq/hintergrundinfos) 10 | 11 | - :heavy_check_mark: Supports Location Information, Stop Events and Trips 12 | - :x: Contract required 13 | - :x: *Really* complex onboarding 14 | 15 | ### [Karlsruher Verkehrsverbund](https://www.kvv.de/fahrplan/fahrplaene/open-data.html) 16 | 17 | - :heavy_check_mark: Supports Location Information, Stop Events and Trips 18 | - :x: Contract required 19 | 20 | ### [Münchner Verkehrsverbund](https://www.mvv-muenchen.de/fahrplanauskunft/fuer-entwickler/index.html) 21 | 22 | Coming soon™. 23 | 24 | ### [Nahverkehr Baden-Württemberg (bwegt)](https://www.mobidata-bw.de/dataset/trias) 25 | 26 | - :heavy_check_mark: Supports Location Information, Stop Events and Trips 27 | - :x: Contract required 28 | 29 | ### [Schweizerische Bundesbahnen](https://opentransportdata.swiss/dataset/aaa) (Switzerland) 30 | 31 | - :heavy_check_mark: Supports Location Information, Stop Events and Trips 32 | - :heavy_check_mark: No contract required 33 | 34 | They provide an open API for testing purposes. Use `https://api.opentransportdata.swiss/trias2020` as URL and `57c5dbbbf1fe4d000100001842c323fa9ff44fbba0b9b925f0c052d1` as `Authorization` header. 35 | 36 | ### [Verkehrsverbund Bremen & Niedersachsen](https://www.vbn.de/service/entwicklerinfos/) 37 | 38 | - :heavy_check_mark: Supports Location Information, Stop Events and Trips 39 | - :heavy_check_mark: No contract required 40 | - :x: Only 3000 requests per day 41 | 42 | ### [Verkehrsverbund Oberelbe](https://www.govdata.de/daten/-/details/api-fahrplanauskunft-vvo) 43 | 44 | - :heavy_check_mark: Supports Location Information, Stop Events and Trips 45 | - :heavy_check_mark: Open Data 46 | 47 | Use `http://efa.vvo-online.de:8080/std3/trias` as URL and `OpenService` as requestor ref. Make sure to set the `Content-Type` header to `text/xml` instead of `application/xml` (which is the default of `trias-client`). 48 | 49 | ### [Verkehrsverbund Region Trier](https://www.vrt-info.de/openservice) 50 | 51 | - :heavy_check_mark: Supports Location Information, Stop Events and Trips 52 | - :heavy_check_mark: No contract required 53 | 54 | ### [Verkehrsverbund Rhein-Neckar](https://www.vrn.de/opendata/API) 55 | 56 | - :heavy_check_mark: Supports Location Information, Stop Events and Trips 57 | - :heavy_check_mark: No contract required 58 | 59 | ### [Verkehrsverbund Rhein-Ruhr](https://openvrr.de/pages/api) 60 | 61 | - :heavy_check_mark: Supports Location Information, Stop Events and Trips 62 | - :grey_question: Contract might be required 63 | - :x: Does not answer to onboarding requests 64 | 65 | They provide an open API for testing purposes. Use `http://openservice-test.vrr.de/opendataT/trias` as URL. 66 | 67 | ### [Verkehrsverbund Rhein-Sieg](https://www.vrs.de/fahren/fahrplanauskunft/opendata-/-openservice) 68 | 69 | - :heavy_check_mark: Supports Location Information, Stop Events and Trips 70 | - :grey_question: Contract might be required 71 | 72 | ### [Verkehrsverbund Steiermark](https://www.verbundlinie.at/fahrplan/rund-um-den-fahrplan/link-zum-fahrplan) (Austria) 73 | 74 | - :heavy_check_mark: Supports Location Information, Stop Events and Trips 75 | - :heavy_check_mark: No contract required 76 | 77 | ### [Verkehrsverbund Stuttgart](https://www.openvvs.de/pages/api) 78 | 79 | - :heavy_check_mark: Supports Location Information, Stop Events and Trips 80 | - :heavy_check_mark: No contract required 81 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Trias Client Documentation 2 | 3 | This Node.js module is written using TypeScript and therefore comes with easy to use type specifications you can have a look at in the [`types`](../src/types) directory. 4 | 5 | Every function requires defined [options](../src/types/options.ts) and returns defined [results](../src/types/results.ts), which then include [Friendly Public Transport Format](https://github.com/public-transport/friendly-public-transport-format) elements. As of now, the FPTF does not fully support all TRIAS functionalities, so be aware that there are some [slight differences](../src/types/fptf.ts). 6 | 7 | ## trias.getClient(ClientOptions options) 8 | 9 | This function returns a client that you can use to perform requests against a TRIAS API. It requires [`ClientOptions`](..src/types/options.ts#L1) and will return a [`TriasClient`](../src/index.ts#L9) instance. 10 | 11 | | Paramater | Description | Type | Required | Default | Example | 12 | |---|---|---|---|---|---| 13 | | url | URL of the TRIAS API. | string | yes | none | `"https://provider.data/trias"` | 14 | | requestorRef | Requestor ref for the TRIAS API. | string | no | none | `"user123"` | 15 | | headers | Custom http headers for the requests to the TRIAS API. | Object | no | none | `{"x-test-header": "myvalue"}` | 16 | 17 | ## client.getStops(StopsRequestOptions options) 18 | 19 | This function returns a list of stops that fit the given search criteria. It requires [`StopsRequestOptions`](../src/types/options.ts#L23) and will return a Promise which resolves into a [`StopsResult`](..src/types/results.ts#L14). 20 | 21 | | Paramater | Description | Type | Required | Default | Example | 22 | |---|---|---|---|---|---| 23 | | name | Name of the stop. Required when no coordinates are provided. | string | no | none | `"Bismarckplatz"` | 24 | | latitude | Latitude for location search. Required when no name is provided. | number | no | none | `49.4098614` | 25 | | longitude | Longitude for location search. Required when no name is provided. | number | no | none | `8.6931989` | 26 | | radius | Radius for location search. | number | no | `500` | `1000` | 27 | | maxResults | Maximum amount of results. | number | no | `10` | `15` | 28 | 29 | ## client.getDepartures(DeparturesRequestOptions options) 30 | 31 | This function returns a list of departures and ticker information for a given stop. It requires [`DeparturesRequestOptions`](../src/types/options.ts#L7) and will return a Promise which resolves into a [`DeparturesResult`](../src/types/results.ts#L5). 32 | 33 | | Paramater | Description | Type | Required | Default | Example | 34 | |---|---|---|---|---|---| 35 | | id | ID of the stop. | string | yes | none | `"de:08222:2417"` | 36 | | time | Requested time for departures as ISO 8601. | string | no | now | `"2021-03-24T21:14:00+01:00` | 37 | | maxResults | Maximum amount of results. | number | no | `25` | `15` | 38 | | includeSituations | Whether you want to retrieve situations. | boolean | no |`false` | `false` | 39 | 40 | ## client.getJourneys(DeparturesRequestOptions options) 41 | 42 | This function returns a list of journeys for given origin and destination stops. It requires [`JourneysRequestOptions`](../src/types/options.ts#L13) and will return a Promise which resolves into a [`JourneysResult`](../src/types/results.ts#L10). 43 | 44 | | Paramater | Description | Type | Required | Default | Example | 45 | |---|---|---|---|---|---| 46 | | origin | ID of the origin stop. | string | yes | none | `"de:08222:2417"` | 47 | | destination | ID of the destination stop. | string | yes | none | `"de:08221:1146"` | 48 | | via | IDs of the in between stops. | array | no | `[]` | `["de:08221:1146"]` | 49 | | arrivalTime | Desired time of arrival as ISO 8601. Overrides departure time. | string | no | none | `"2021-03-24T21:14:00+01:00` | 50 | | departureTime | Desired time of departure as ISO 8601. Only considered if arrival time is not set. | string | no | now | `"2021-03-24T23:08:00+01:00` | 51 | | maxResults | Maximum amount of results. | number | no | `5` | `15` | 52 | | includeFares | Whether you want to retrieve fares. | boolean | no |`false` | `false` | 53 | | includeSituations | Whether you want to retrieve situations. | boolean | no |`false` | `false` | 54 | -------------------------------------------------------------------------------- /docs/example.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from "util"; 2 | import { getClient } from "../lib"; 3 | 4 | const sbbProfile = { 5 | // Test environment of SBB 6 | // See here: https://opentransportdata.swiss/de/cookbook/abfahrts-ankunftsanzeiger/ 7 | url: "https://api.opentransportdata.swiss/trias2020", 8 | requestorRef: "trias-client", 9 | headers: { 10 | authorization: "57c5dbbbf1fe4d000100001842c323fa9ff44fbba0b9b925f0c052d1", 11 | } 12 | }; 13 | 14 | const zürich = "8503000"; 15 | const luzern = "8505000"; 16 | const aarau = "8502113"; 17 | 18 | const client = getClient(sbbProfile); 19 | 20 | // To Do: Add examples for station search and departures 21 | 22 | async () => { 23 | const journeys = await client.getJourneys({ 24 | origin: zürich, 25 | destination: luzern, 26 | via: [aarau], 27 | departureTime: "2021-05-20T14:00+02:00", 28 | maxResults: 2, 29 | includeFares: true, 30 | }); 31 | 32 | console.log(inspect(journeys, { depth: null, colors: true })); 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trias-client", 3 | "version": "0.5.0", 4 | "description": "Lean Node.js client for public transport TRIAS APIs.", 5 | "keywords": [ 6 | "public-transport", 7 | "trias" 8 | ], 9 | "main": "lib/index.js", 10 | "scripts": { 11 | "build": "tsc", 12 | "format": "prettier --write \"src/**/*.ts\"", 13 | "lint": "eslint .", 14 | "test": "tsc && jest" 15 | }, 16 | "jest": { 17 | "testEnvironment": "node", 18 | "coveragePathIgnorePatterns": [ 19 | "/node_modules/" 20 | ], 21 | "verbose": true 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/andaryjo/trias-client.git" 26 | }, 27 | "author": "Andary", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/andaryjo/trias-client/issues" 31 | }, 32 | "devDependencies": { 33 | "@types/lodash": "^4.14.168", 34 | "@types/node": "^14.14.35", 35 | "@typescript-eslint/eslint-plugin": "^4.22.0", 36 | "@typescript-eslint/parser": "^4.22.0", 37 | "eslint": "^7.25.0", 38 | "eslint-config-prettier": "^8.3.0", 39 | "eslint-plugin-jest": "^24.3.6", 40 | "friendly-public-transport-format": "^1.2.1", 41 | "jest": "^26.6.3", 42 | "prettier": "^2.2.1", 43 | "ts-node": "^9.1.1", 44 | "typescript": "^4.2.3" 45 | }, 46 | "dependencies": { 47 | "@types/xmldom": "^0.1.30", 48 | "axios": "^0.21.1", 49 | "css-select": "^4.1.2", 50 | "domhandler": "^4.2.0", 51 | "htmlparser2": "^6.1.0", 52 | "lodash": "^4.17.21", 53 | "moment-timezone": "^0.5.33" 54 | }, 55 | "files": [ 56 | "lib/**/*" 57 | ] 58 | } -------------------------------------------------------------------------------- /src/RequestAndParse.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; 2 | import { Document, Node as DOMNode, Element as DOMElement, DomHandler as DOMHandler, DomHandlerOptions as DOMHandlerOptions } from "domhandler"; 3 | import { Options as CssSelectOptions, selectAll as _selectAll, selectOne as _selectOne, compile as _compile } from "css-select"; 4 | import { CompiledQuery } from "css-select/lib/types"; 5 | import { getText as _getText } from "domutils"; 6 | import { Parser, ParserOptions } from "htmlparser2"; 7 | 8 | export { Element as DOMElement } from "domhandler"; 9 | 10 | const DEBUG = /(^|,)trias-client(,|$)/.test(process.env.DEBUG || ""); 11 | 12 | export async function request(url: string, requestorRef: string, headers: { [key: string]: string }, reqBody: string): Promise> { 13 | 14 | // Convert all header keys to lower case, to make sure that you actually overwrite the content-type header when specifying Content-Type 15 | // HTTP headers are case-insensitive, so this shouldn't be a problem 16 | for (const header in headers) { 17 | if (header == header.toLowerCase()) continue; 18 | headers[header.toLowerCase()] = headers[header]; 19 | delete headers.header; 20 | } 21 | 22 | const req: AxiosRequestConfig = { 23 | url, 24 | method: "POST", 25 | headers: { 26 | // There are two MIME assignments for XML data. These are: 27 | // - application/xml (RFC 7303, previously RFC 3023) 28 | // - text/xml (RFC 7303, previously RFC 3023) 29 | // https://en.wikipedia.org/wiki/XML_and_MIME 30 | "content-type": "application/xml", 31 | "accept": "application/xml", 32 | ...headers, 33 | }, 34 | data: reqBody, 35 | }; 36 | 37 | // tslint:disable-next-line:no-console 38 | if (DEBUG) console.error(reqBody); 39 | 40 | const res = await axios(req); 41 | 42 | // tslint:disable-next-line:no-console 43 | if (DEBUG) console.error(res.data); 44 | 45 | return res; 46 | } 47 | 48 | export function selectAll(query: string, elements: DOMNode | DOMNode[] | null, options: CssSelectOptions = {}): DOMElement[] { 49 | if (elements === null) return []; 50 | return _selectAll(query, elements, { 51 | xmlMode: true, 52 | ...options, 53 | }); 54 | } 55 | export function selectOne(query: string, elements: DOMNode | DOMNode[] | null, options: CssSelectOptions = {}): DOMElement | null { 56 | if (elements === null) return null; 57 | return _selectOne(query, elements, { 58 | xmlMode: true, 59 | ...options, 60 | }); 61 | } 62 | export function compile(selector: string, options: CssSelectOptions = {}, context: DOMNode | DOMNode[] | undefined): CompiledQuery { 63 | return _compile( 64 | selector, 65 | { 66 | xmlMode: true, 67 | ...options, 68 | }, 69 | context, 70 | ); 71 | } 72 | 73 | export function getText(node: DOMNode | DOMNode[] | null): string | null { 74 | return node ? _getText(node) : null; 75 | } 76 | 77 | // https://github.com/fb55/htmlparser2/blob/ee6879069b4d30ecb327ca1426747791f45d3920/src/index.ts#L18-L28 78 | export function parseResponse(data: string): Document { 79 | const handler = new DOMHandler(null, { 80 | withStartIndices: false, 81 | withEndIndices: false, 82 | }); 83 | const onOpenTag = handler.onopentag.bind(handler); 84 | // https://github.com/fb55/domhandler/blob/7aec3ae0f4ac59325f04d833a6e10f767a49d035/src/index.ts#L160-L165 85 | handler.onopentag = (name: string, attribs: { [key: string]: string }): void => { 86 | onOpenTag(name.replace(/^trias:/, ""), attribs); 87 | }; 88 | 89 | const parser = new Parser(handler, { 90 | xmlMode: true, 91 | decodeEntities: true, 92 | }); 93 | parser.end(data); 94 | return handler.root; 95 | } 96 | 97 | export async function requestAndParse(url: string, requestorRef: string, headers: { [key: string]: string }, reqBody: string): Promise { 98 | const res = await request(url, requestorRef, headers, reqBody); 99 | return parseResponse(res.data); 100 | } 101 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { TRIASDeparturesHandler } from "./trias/TRIASDeparturesHandler"; 2 | import { TRIASJourneysHandler } from "./trias/TRIASJourneysHandler"; 3 | import { TRIASStopsHandler } from "./trias/TRIASStopsHandler"; 4 | 5 | export const getClient = (options: ClientOptions) : TRIASClient => { 6 | return new TRIASClient(options); 7 | }; 8 | 9 | class TRIASClient { 10 | departuresHandler; 11 | journeysHandler; 12 | stopsHandler; 13 | 14 | constructor(options: ClientOptions) { 15 | if (!options.requestorRef) options.requestorRef = ""; 16 | if (!options.headers) options.headers = {}; 17 | 18 | this.departuresHandler = new TRIASDeparturesHandler(options.url, options.requestorRef, options.headers); 19 | this.journeysHandler = new TRIASJourneysHandler(options.url, options.requestorRef, options.headers); 20 | this.stopsHandler = new TRIASStopsHandler(options.url, options.requestorRef, options.headers); 21 | } 22 | 23 | getDepartures(options: DeparturesRequestOptions) { 24 | return this.departuresHandler.getDepartures(options); 25 | } 26 | 27 | getJourneys(options: JourneyRequestOptions) { 28 | return this.journeysHandler.getJourneys(options); 29 | } 30 | 31 | getStops(options: StopsRequestOptions) { 32 | return this.stopsHandler.getStops(options); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/trias/TRIASDeparturesHandler.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment-timezone"; 2 | 3 | import { requestAndParse, selectAll, selectOne, getText } from "../RequestAndParse"; 4 | import { TRIAS_SER } from "../xml/TRIAS_SER"; 5 | 6 | export class TRIASDeparturesHandler { 7 | url: string; 8 | requestorRef: string; 9 | headers: { [key: string]: string }; 10 | 11 | constructor(url: string, requestorRef: string, headers: { [key: string]: string }) { 12 | this.url = url; 13 | this.requestorRef = requestorRef; 14 | this.headers = headers; 15 | } 16 | 17 | async getDepartures(options: DeparturesRequestOptions): Promise { 18 | const maxResults = options.maxResults ? options.maxResults : 20; 19 | 20 | let time; 21 | if (options.time) time = moment(options.time).tz("Europe/Berlin").format("YYYY-MM-DDTHH:mm:ss"); 22 | else time = moment().tz("Europe/Berlin").format("YYYY-MM-DDTHH:mm:ss"); 23 | 24 | const payload = TRIAS_SER.replace("$STATIONID", options.id).replace("$TIME", time).replace("$MAXRESULTS", maxResults.toString()).replace("$TOKEN", this.requestorRef); 25 | 26 | const doc = await requestAndParse(this.url, this.requestorRef, this.headers, payload); 27 | 28 | const situations: Situation[] = []; 29 | const departures: FPTFStopover[] = []; 30 | 31 | if (options.includeSituations) { 32 | for (const situationEl of selectAll("PtSituation", doc)) { 33 | 34 | const summary = getText(selectOne("Summary", situationEl)); 35 | const detail = getText(selectOne("Detail", situationEl)); 36 | const startTime = getText(selectOne("StartTime", situationEl)); 37 | const endTime = getText(selectOne("EndTime", situationEl)); 38 | const priority = getText(selectOne("Priority", situationEl)); 39 | 40 | const situation: Situation = { 41 | title: summary || "", 42 | description: detail || "", 43 | validFrom: startTime || "", 44 | validTo: endTime || "", 45 | priority: priority || "" 46 | } 47 | 48 | situations.push(situation); 49 | } 50 | } 51 | 52 | for (const departureEl of selectAll("StopEvent", doc)) { 53 | const departure: FPTFStopover = { 54 | type: "stopover", 55 | stop: options.id, 56 | line: { 57 | type: "line", 58 | id: "", 59 | line: "", 60 | }, 61 | mode: FPTFMode.UNKNOWN, 62 | direction: "", 63 | departure: "", 64 | }; 65 | 66 | const lineName = getText(selectOne("PublishedLineName Text", departureEl)) || getText(selectOne("Name Text", departureEl)); 67 | if (lineName && departure.line) { 68 | departure.line.id = lineName; 69 | departure.line.line = lineName; 70 | } 71 | 72 | const direction = getText(selectOne("DestinationText Text", departureEl)); 73 | if (direction) departure.direction = direction; 74 | 75 | const timetabledTime = getText(selectOne("TimetabledTime", departureEl)); 76 | if (timetabledTime) departure.departure = this.parseResponseTime(timetabledTime); 77 | 78 | const estimatedTime = getText(selectOne("EstimatedTime", departureEl)); 79 | if (estimatedTime) departure.departureDelay = moment(estimatedTime).unix() - moment(timetabledTime).unix(); 80 | 81 | const plannedBay = getText(selectOne("PlannedBay Text", departureEl)); 82 | if (plannedBay) departure.departurePlatform = plannedBay; 83 | 84 | const type = getText(selectOne("PtMode", departureEl)); 85 | if (type === "bus") { 86 | departure.mode = FPTFMode.BUS; 87 | } else if (type === "tram") { 88 | departure.mode = FPTFMode.TRAIN; 89 | departure.subMode = FPTFSubmode.TRAM; 90 | } else if (type === "metro") { 91 | departure.mode = FPTFMode.TRAIN; 92 | departure.subMode = FPTFSubmode.METRO; 93 | } else if (type === "rail") { 94 | departure.mode = FPTFMode.TRAIN; 95 | departure.subMode = FPTFSubmode.RAIL; 96 | } 97 | 98 | departures.push(departure); 99 | } 100 | 101 | const result: DeparturesResult = { 102 | success: true, 103 | departures, 104 | } 105 | if (options.includeSituations) result.situations = situations; 106 | 107 | return result; 108 | } 109 | 110 | parseResponseTime(time: string): string { 111 | return moment(time).tz("Europe/Berlin").format(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/trias/TRIASJourneysHandler.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment-timezone"; 2 | import { resourceLimits } from "node:worker_threads"; 3 | 4 | import { requestAndParse, selectAll, selectOne, getText, DOMElement } from "../RequestAndParse"; 5 | import { TRIAS_TR } from "../xml/TRIAS_TR"; 6 | 7 | export class TRIASJourneysHandler { 8 | url: string; 9 | requestorRef: string; 10 | headers: { [key: string]: string }; 11 | 12 | constructor(url: string, requestorRef: string, headers: { [key: string]: string }) { 13 | this.url = url; 14 | this.requestorRef = requestorRef; 15 | this.headers = headers; 16 | } 17 | 18 | async getJourneys(options: JourneyRequestOptions): Promise { 19 | const maxResults = options.maxResults ? options.maxResults : 5; 20 | 21 | let arrTime; 22 | let depTime; 23 | if (options.arrivalTime) arrTime = this.parseRequestTime(options.arrivalTime); 24 | else if (options.departureTime) depTime = this.parseRequestTime(options.departureTime); 25 | 26 | const via = (options.via || []) 27 | .map((stationID) => this.parseRequestViaStation(stationID)).join(""); 28 | 29 | const payload = TRIAS_TR.replace("$ORIGIN", options.origin) 30 | .replace("$VIA", via) 31 | .replace("$DESTINATION", options.destination) 32 | .replace("$DEPTIME", depTime ? depTime : "") 33 | .replace("$ARRTIME", arrTime ? arrTime : "") 34 | .replace("$MAXRESULTS", maxResults.toString()) 35 | .replace("$INCLUDE_FARES", options.includeFares ? "true" : "false") 36 | .replace("$TOKEN", this.requestorRef); 37 | 38 | const doc = await requestAndParse(this.url, this.requestorRef, this.headers, payload); 39 | 40 | const situations: Situation[] = []; 41 | const trips: FPTFJourney[] = []; 42 | 43 | if (options.includeSituations) { 44 | for (const situationEl of selectAll("PtSituation", doc)) { 45 | 46 | const summary = getText(selectOne("Summary", situationEl)); 47 | const detail = getText(selectOne("Detail", situationEl)); 48 | const startTime = getText(selectOne("StartTime", situationEl)); 49 | const endTime = getText(selectOne("EndTime", situationEl)); 50 | const priority = getText(selectOne("Priority", situationEl)); 51 | 52 | const situation: Situation = { 53 | title: summary || "", 54 | description: detail || "", 55 | validFrom: startTime || "", 56 | validTo: endTime || "", 57 | priority: priority || "" 58 | } 59 | 60 | situations.push(situation); 61 | } 62 | } 63 | 64 | for (const tripEl of selectAll("TripResult", doc)) { 65 | const trip: FPTFJourney = { 66 | type: "journey", 67 | id: "", 68 | legs: [], 69 | }; 70 | 71 | const tripID = getText(selectOne("TripId", tripEl)); 72 | if (tripID) trip.id = tripID; 73 | 74 | for (const legEl of selectAll("TripLeg", tripEl)) { 75 | const leg: FPTFLeg = { 76 | mode: FPTFMode.UNKNOWN, 77 | direction: "", 78 | origin: "", 79 | destination: "", 80 | departure: "", 81 | arrival: "", 82 | }; 83 | 84 | if (selectOne("TimedLeg", legEl)) { 85 | const origin: FPTFStop = { 86 | type: "stop", 87 | id: "", 88 | name: "", 89 | }; 90 | 91 | const legBoardEl = selectOne("LegBoard", legEl); 92 | 93 | const startStationID = getText(selectOne("StopPointRef", legBoardEl)); 94 | if (startStationID) origin.id = this.parseStationID(startStationID); 95 | 96 | const startStationName = getText(selectOne("StopPointName Text", legBoardEl)); 97 | if (startStationName) origin.name = startStationName; 98 | 99 | const startTime = getText(selectOne("TimetabledTime", legBoardEl)); 100 | if (startTime) leg.departure = this.parseResponseTime(startTime); 101 | 102 | const startRealtime = getText(selectOne("EstimatedTime", legBoardEl)); 103 | if (startRealtime) leg.departureDelay = moment(startRealtime).unix() - moment(leg.departure).unix(); 104 | 105 | const startPlatform = getText(selectOne("PlannedBay Text", legBoardEl)); 106 | if (startPlatform) leg.departurePlatform = startPlatform; 107 | 108 | const destination: FPTFStop = { 109 | type: "stop", 110 | id: "", 111 | name: "", 112 | }; 113 | 114 | const legAlightEl = selectOne("LegAlight", legEl); 115 | 116 | const endStationID = getText(selectOne("StopPointRef", legAlightEl)); 117 | if (endStationID) destination.id = this.parseStationID(endStationID); 118 | 119 | const endStationName = getText(selectOne("StopPointName Text", legAlightEl)); 120 | if (endStationName) destination.name = endStationName; 121 | 122 | const endTime = getText(selectOne("TimetabledTime", legAlightEl)); 123 | if (endTime) leg.arrival = this.parseResponseTime(endTime); 124 | 125 | const endRealtime = getText(selectOne("EstimatedTime", legAlightEl)); 126 | if (endRealtime) leg.arrivalDelay = moment(endRealtime).unix() - moment(leg.arrival).unix(); 127 | 128 | const endPlatform = getText(selectOne("PlannedBay Text", legAlightEl)); 129 | if (endPlatform) leg.arrivalPlatform = endPlatform; 130 | 131 | leg.line = { 132 | type: "line", 133 | id: "", 134 | line: "", 135 | }; 136 | 137 | const lineName = getText(selectOne("PublishedLineName Text", legEl)) || getText(selectOne("Name Text", legEl)); 138 | if (lineName && leg.line) { 139 | leg.line.id = lineName; 140 | leg.line.line = lineName; 141 | } 142 | 143 | const direction = getText(selectOne("DestinationText Text", legEl)); 144 | if (direction) leg.direction = direction; 145 | 146 | const mode = getText(selectOne("PtMode", legEl)); 147 | if (mode === "bus") { 148 | leg.mode = FPTFMode.BUS; 149 | } else if (mode === "tram") { 150 | leg.mode = FPTFMode.TRAIN; 151 | leg.subMode = FPTFSubmode.TRAM; 152 | } else if (mode === "metro") { 153 | leg.mode = FPTFMode.TRAIN; 154 | leg.subMode = FPTFSubmode.METRO; 155 | } else if (mode === "rail") { 156 | leg.mode = FPTFMode.TRAIN; 157 | leg.subMode = FPTFSubmode.RAIL; 158 | } 159 | 160 | leg.origin = origin; 161 | leg.destination = destination; 162 | } else if (selectOne("ContinuousLeg", legEl) || selectOne("InterchangeLeg", legEl)) { 163 | const origin: FPTFLocation = { 164 | type: "location", 165 | name: "", 166 | }; 167 | 168 | const legStartEl = selectOne("LegStart", legEl); 169 | 170 | const startLocationName = getText(selectOne("LocationName Text", legStartEl)); 171 | if (startLocationName) origin.name = startLocationName; 172 | 173 | const startGeoPos = selectOne("GeoPosition", legStartEl); 174 | if (startGeoPos) { 175 | const latitude = getText(selectOne("Latitude", startGeoPos)); 176 | if (latitude) origin.latitude = parseFloat(latitude); 177 | 178 | const longitude = getText(selectOne("Longitude", startGeoPos)); 179 | if (longitude) origin.longitude = parseFloat(longitude); 180 | } 181 | 182 | const destination: FPTFLocation = { 183 | type: "location", 184 | name: "", 185 | }; 186 | 187 | const legEndEl = selectOne("LegEnd", legEl); 188 | 189 | const endLocationName = getText(selectOne("LocationName Text", legEl)); 190 | if (endLocationName) destination.name = endLocationName; 191 | 192 | const endGeoPos = selectOne("GeoPosition", legEndEl); 193 | if (endGeoPos) { 194 | const latitude = getText(selectOne("Latitude", endGeoPos)); 195 | if (latitude) destination.latitude = parseFloat(latitude); 196 | 197 | const longitude = getText(selectOne("Longitude", endGeoPos)); 198 | if (longitude) destination.longitude = parseFloat(longitude); 199 | } 200 | 201 | const startTime = getText(selectOne("TimeWindowStart", legEl)); 202 | if (startTime) leg.departure = this.parseResponseTime(startTime); 203 | 204 | const endTime = getText(selectOne("TimeWindowEnd", legEl)); 205 | if (endTime) leg.arrival = this.parseResponseTime(endTime); 206 | 207 | leg.mode = FPTFMode.WALKING; 208 | 209 | leg.origin = origin; 210 | leg.destination = destination; 211 | } 212 | 213 | trip.legs.push(leg); 214 | } 215 | 216 | if (options.includeFares) { 217 | if (!trip.tickets) trip.tickets = []; 218 | 219 | // todo: there might be multiple 220 | const faresEl = selectOne("TripFares", tripEl); 221 | for (const ticketEl of selectAll("Ticket", faresEl)) { 222 | const ticket = this.parseResponseTicket(ticketEl); 223 | if (ticket) trip.tickets.push(ticket); 224 | } 225 | } 226 | 227 | trips.push(trip); 228 | } 229 | 230 | const result: JourneysResult = { 231 | success: true, 232 | journeys: trips, 233 | } 234 | if (options.includeSituations) result.situations = situations; 235 | 236 | return result; 237 | } 238 | 239 | parseStationID(id: string): string { 240 | if (!id.includes(":")) return id; 241 | const t = id.split(":"); 242 | return t[0] + ":" + t[1] + ":" + t[2]; 243 | } 244 | 245 | parseRequestViaStation(stationID: string): string { 246 | return "" + stationID + ""; 247 | } 248 | 249 | parseRequestTime(time: string): string { 250 | return "" + moment(time).tz("Europe/Berlin").format("YYYY-MM-DDTHH:mm:ss") + ""; 251 | } 252 | 253 | parseResponseTime(time: string): string { 254 | return moment(time).tz("Europe/Berlin").format(); 255 | } 256 | 257 | parseResponseTicket(ticketEl: DOMElement): Ticket | null { 258 | const id = getText(selectOne("TicketId", ticketEl)); 259 | const name = getText(selectOne("TicketName", ticketEl)); 260 | const faresAuthorityRef = getText(selectOne("FaresAuthorityRef", ticketEl)); 261 | const faresAuthorityName = getText(selectOne("FaresAuthorityText", ticketEl)); 262 | if (!id || !name || !faresAuthorityRef || !faresAuthorityName) return null; 263 | const price = getText(selectOne("Price", ticketEl)); 264 | return { 265 | id, 266 | name, 267 | faresAuthorityRef, 268 | faresAuthorityName, 269 | price: price ? parseFloat(price) : null, 270 | currency: getText(selectOne("Currency", ticketEl)), 271 | tariffLevel: getText(selectOne("TariffLevel", ticketEl)), 272 | travelClass: getText(selectOne("TravelClass", ticketEl)), 273 | validFor: getText(selectOne("ValidFor", ticketEl)), 274 | validityDuration: getText(selectOne("ValidityDuration", ticketEl)), 275 | }; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/trias/TRIASStopsHandler.ts: -------------------------------------------------------------------------------- 1 | import { requestAndParse, selectAll, selectOne, getText } from "../RequestAndParse"; 2 | import { TRIAS_LIR_NAME } from "../xml/TRIAS_LIR_NAME"; 3 | import { TRIAS_LIR_POS } from "../xml/TRIAS_LIR_POS"; 4 | 5 | export class TRIASStopsHandler { 6 | url: string; 7 | requestorRef: string; 8 | headers: { [key: string]: string }; 9 | 10 | constructor(url: string, requestorRef: string, headers: { [key: string]: string }) { 11 | this.url = url; 12 | this.requestorRef = requestorRef; 13 | this.headers = headers; 14 | } 15 | 16 | async getStops(options: StopsRequestOptions): Promise { 17 | const maxResults = options.maxResults ? options.maxResults : 10; 18 | let payload; 19 | 20 | if (options.name) payload = TRIAS_LIR_NAME.replace("$QUERY", options.name).replace("$MAXRESULTS", maxResults.toString()).replace("$TOKEN", this.requestorRef); 21 | else if (options.latitude && options.longitude && options.radius) 22 | payload = TRIAS_LIR_POS.replace("$LATITUDE", options.latitude.toString()) 23 | .replace("$LONGITUDE", options.longitude.toString()) 24 | .replace("$RADIUS", options.radius.toString()) 25 | .replace("$MAXRESULTS", maxResults.toString()) 26 | .replace("$TOKEN", this.requestorRef); 27 | else { 28 | throw new Error("options.name or options.{latitude,longitude} must be passed"); 29 | } 30 | 31 | const doc = await requestAndParse(this.url, this.requestorRef, this.headers, payload); 32 | 33 | const stops: FPTFStop[] = []; 34 | 35 | for (const locationEl of selectAll("LocationResult", doc)) { 36 | const stop: FPTFStop = { 37 | type: "stop", 38 | id: "", 39 | name: "", 40 | }; 41 | 42 | const id = getText(selectOne("StopPointRef", locationEl)); 43 | if (id) stop.id = id; 44 | 45 | const latitude = getText(selectOne("Latitude", locationEl)); 46 | const longitude = getText(selectOne("Longitude", locationEl)); 47 | if (latitude && longitude) { 48 | stop.location = { 49 | type: "location", 50 | latitude: parseFloat(latitude), 51 | longitude: parseFloat(longitude), 52 | }; 53 | } 54 | 55 | const stationName = getText(selectOne("StopPointName Text", locationEl)); 56 | const locationName = getText(selectOne("LocationName Text", locationEl)); 57 | 58 | if (locationName && stationName && !stationName.includes(locationName)) stop.name = locationName + " " + stationName; 59 | else if (stationName) stop.name = stationName; 60 | 61 | stops.push(stop); 62 | } 63 | 64 | return { 65 | success: true, 66 | stops, 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/types/fptf.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Using the Friendly Public Transport Format (FPTF) v1.2.1 for all responses 3 | * However, some optional attributes were removed as they are not supported by TRIAS 4 | * See https://github.com/public-transport/friendly-public-transport-format/tree/1.2.1 5 | * 6 | * For reference, this data model mostly aligns with the one of Abfahrt Core, which has been used in TRIAS use cases for a long time 7 | * See https://gitlab.com/andary/abfahrt-core/-/blob/2.0/swagger.yaml 8 | */ 9 | 10 | interface FPTFStop { 11 | type: string; 12 | id: string; 13 | name: string; 14 | location?: FPTFLocation; 15 | } 16 | 17 | interface FPTFLocation { 18 | type: string; 19 | name?: string; 20 | address?: string; 21 | longitude?: number; 22 | latitude?: number; 23 | altitude?: number; 24 | } 25 | 26 | interface FPTFLine { 27 | type: string; 28 | id: string; 29 | line: string; 30 | } 31 | 32 | interface FPTFStopover { 33 | type: string; 34 | stop: string; 35 | line?: FPTFLine; // Not included in FPTF 36 | mode: FPTFMode; 37 | subMode?: FPTFSubmode; 38 | direction?: string; // Not included in FPTF 39 | arrival?: string; 40 | arrivalDelay?: number; 41 | arrivalPlatform?: string; 42 | departure?: string; 43 | departureDelay?: number; 44 | departurePlatform?: string; 45 | } 46 | 47 | interface FPTFJourney { 48 | type: string; 49 | id: string; 50 | legs: FPTFLeg[]; 51 | tickets?: Ticket[]; // Not included in FPTF 52 | } 53 | 54 | interface FPTFLeg { 55 | line?: FPTFLine; // Not included in FPTF 56 | mode: FPTFMode; 57 | subMode?: FPTFSubmode; 58 | direction: string; // Not included in FPTF 59 | origin: string | FPTFStop | FPTFLocation; 60 | destination: string | FPTFStop | FPTFLocation; 61 | departure: string; 62 | departureDelay?: number; 63 | departurePlatform?: string; 64 | arrival: string; 65 | arrivalDelay?: number; 66 | arrivalPlatform?: string; 67 | stopovers?: FPTFStopover[]; 68 | } 69 | 70 | const enum FPTFMode { 71 | AIRCRAFT = "aircraft", 72 | BICYCLE = "bicycle", 73 | BUS = "bus", 74 | CAR = "car", 75 | GONDOLA = "gondola", 76 | TAXI = "taxi", 77 | TRAIN = "train", 78 | UNKNOWN = "unknown", // Not included in FPTF 79 | WALKING = "walking", 80 | WATERCRAFT = "watercraft", 81 | } 82 | 83 | // To be defined in FPTF 84 | const enum FPTFSubmode { 85 | METRO = "metro", 86 | RAIL = "rail", 87 | TRAM = "tram", 88 | } 89 | 90 | // Not included in FPTF 91 | interface Ticket { 92 | id: string; 93 | name: string; 94 | faresAuthorityRef: string; 95 | faresAuthorityName: string; 96 | price: number | null; 97 | // todo: 98 | currency: string | null; 99 | // todo: 100 | tariffLevel: string | null; 101 | // todo: 102 | travelClass: string | null; // todo: make an enum 103 | // todo: 104 | validFor: string | null; // todo: make an enum 105 | validityDuration: string | null; 106 | // todo: 107 | // todo: 108 | // todo: 109 | // todo: 110 | // todo: 111 | // todo: 112 | } 113 | 114 | interface Situation { 115 | title: string, 116 | description: string; 117 | validFrom: string; 118 | validTo: string; 119 | priority: string; 120 | } 121 | -------------------------------------------------------------------------------- /src/types/options.ts: -------------------------------------------------------------------------------- 1 | interface ClientOptions { 2 | url: string; 3 | requestorRef?: string; 4 | headers?: { [key: string]: string }; 5 | } 6 | 7 | interface DeparturesRequestOptions { 8 | id: string; 9 | time?: string; 10 | maxResults?: number; 11 | includeSituations?: boolean; 12 | } 13 | 14 | interface JourneyRequestOptions { 15 | origin: string; 16 | destination: string; 17 | via?: string[]; 18 | departureTime?: string; 19 | arrivalTime?: string; 20 | maxResults?: number; 21 | includeFares?: boolean; 22 | includeSituations?: boolean; 23 | } 24 | 25 | interface StopsRequestOptions { 26 | name?: string; 27 | latitude?: number; 28 | longitude?: number; 29 | radius?: number; 30 | maxResults?: number; 31 | } 32 | -------------------------------------------------------------------------------- /src/types/results.ts: -------------------------------------------------------------------------------- 1 | interface Result { 2 | success: boolean; 3 | } 4 | 5 | interface DeparturesResult extends Result { 6 | departures?: FPTFStopover[]; 7 | situations?: Situation[]; 8 | } 9 | 10 | interface JourneysResult extends Result { 11 | journeys?: FPTFJourney[]; 12 | situations?: Situation[]; 13 | } 14 | 15 | interface StopsResult extends Result { 16 | stops?: FPTFStop[]; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/xml/TRIAS_LIR_NAME.ts: -------------------------------------------------------------------------------- 1 | export const TRIAS_LIR_NAME = ` 2 | 3 | 4 | 5 | $TOKEN 6 | 7 | 8 | 9 | $QUERY 10 | 11 | 12 | stop 13 | $MAXRESULTS 14 | 15 | 16 | 17 | 18 | 19 | `; 20 | -------------------------------------------------------------------------------- /src/xml/TRIAS_LIR_POS.ts: -------------------------------------------------------------------------------- 1 | export const TRIAS_LIR_POS = ` 2 | 3 | 4 | 5 | $TOKEN 6 | 7 | 8 | 9 | 10 | 11 |
12 | $LONGITUDE 13 | $LATITUDE 14 |
15 | $RADIUS 16 |
17 |
18 |
19 | 20 | stop 21 | $MAXRESULTS 22 | 23 |
24 |
25 |
26 |
27 | `; 28 | -------------------------------------------------------------------------------- /src/xml/TRIAS_SER.ts: -------------------------------------------------------------------------------- 1 | export const TRIAS_SER = ` 2 | 3 | 4 | 5 | $TOKEN 6 | 7 | 8 | 9 | 10 | $STATIONID 11 | 12 | $TIME 13 | 14 | 15 | true 16 | $MAXRESULTS 17 | departure 18 | 19 | 20 | 21 | 22 | 23 | `; 24 | -------------------------------------------------------------------------------- /src/xml/TRIAS_TR.ts: -------------------------------------------------------------------------------- 1 | export const TRIAS_TR = ` 2 | 3 | 4 | 5 | $TOKEN 6 | 7 | 8 | 9 | 10 | $ORIGIN 11 | 12 | $DEPTIME 13 | 14 | $VIA 15 | 16 | 17 | $DESTINATION 18 | 19 | $ARRTIME 20 | 21 | 22 | false 23 | false 24 | false 25 | false 26 | $INCLUDE_FARES 27 | $MAXRESULTS 28 | 29 | 30 | 31 | 32 | 33 | `; 34 | -------------------------------------------------------------------------------- /test/client.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const moment = require("moment-timezone"); 3 | const trias = require("../lib/index.js"); 4 | 5 | const creds = process.env.TEST_CREDENTIALS 6 | ? JSON.parse(process.env.TEST_CREDENTIALS) 7 | : require("./test-credentials.json"); 8 | 9 | describe("Test providers", () => { 10 | 11 | jest.setTimeout(10000); 12 | 13 | const providers = [ 14 | { 15 | name: "KVV", 16 | url: creds["KVV"].url, 17 | requestorRef: creds["KVV"].token, 18 | searchName: "karlsruhe", 19 | journeyOrigin: "de:08212:1103", 20 | journeyDestination: "de:08212:89" 21 | }, { 22 | name: "SBB", 23 | url: creds["SBB"].url, 24 | requestorRef: "trias-client", 25 | headers: { "Authorization": creds["SBB"].token }, 26 | searchName: "messeplatz", 27 | journeyOrigin: "8500010", // Basel, Hbf 28 | journeyDestination: "8591442" // Zürich, Zoo 29 | }, { 30 | name: "VRN", 31 | url: creds["VRN"].url, 32 | requestorRef: creds["VRN"].token, 33 | searchName: "bismarckplatz", 34 | journeyOrigin: "de:08222:2432", // Mannheim, Lange Rötterstraße 35 | journeyVia: "de:08221:1146", // Heidelberg, Bismarckplatz 36 | journeyDestination: "de:08221:1283" // Heidelberg, Jägerhaus 37 | }, { 38 | name: "VRR", 39 | url: creds["VRR"].url, 40 | requestorRef: creds["VRR"].token, 41 | searchName: "bahnhof", 42 | journeyOrigin: "de:05314:63101", 43 | journeyDestination: "de:05382:55101" 44 | }, { 45 | name: "VST", 46 | url: creds["VST"].url, 47 | requestorRef: creds["VST"].token, 48 | searchName: "villach", 49 | journeyOrigin: "at:42:3654", // Villach, Hbf 50 | journeyDestination: "at:42:3642" // Klagenfurt, Hbf 51 | }, { 52 | name: "VVO", 53 | url: creds["VVO"].url, 54 | requestorRef: creds["VVO"].token, 55 | headers: { "Content-Type": "text/xml" }, 56 | searchName: "dresden", 57 | journeyOrigin: "de:14612:28", // Dresden, Hbf 58 | journeyDestination: "de:14713:8010205" // Leipzig, Hbf 59 | } 60 | ] 61 | 62 | for (const provider of providers) { 63 | 64 | it("Test " + provider.name, async () => { 65 | 66 | const client = trias.getClient({ 67 | url: provider.url, 68 | requestorRef: provider.requestorRef, 69 | headers: provider.headers 70 | }); 71 | 72 | // Test stop search 73 | const stopsResult = await client.getStops({ 74 | name: provider.searchName 75 | }); 76 | 77 | expect(stopsResult.success).toEqual(true); 78 | expect(stopsResult.stops.length).toBeGreaterThanOrEqual(1); 79 | expect(stopsResult.stops[0].type).toEqual("stop"); 80 | 81 | // Test departures (now) 82 | const departuresNowResult = await client.getDepartures({ 83 | id: stopsResult.stops[0].id, 84 | includeSituations: true 85 | }); 86 | 87 | expect(departuresNowResult.success).toEqual(true); 88 | expect(departuresNowResult.situations.length).toBeGreaterThanOrEqual(0); 89 | expect(departuresNowResult.departures.length).toBeGreaterThanOrEqual(1); 90 | expect(departuresNowResult.departures[0].type).toEqual("stopover"); 91 | 92 | // Test departures (in 30 mins) 93 | const in30Mins = moment().unix() + 30 * 60; 94 | const departuresIn30MinsResult = await client.getDepartures({ 95 | id: stopsResult.stops[0].id, 96 | time: moment.unix(in30Mins).format() 97 | }); 98 | 99 | expect(departuresIn30MinsResult.success).toEqual(true); 100 | expect(departuresIn30MinsResult.departures.length).toBeGreaterThanOrEqual(1); 101 | expect(departuresIn30MinsResult.departures[0].type).toEqual("stopover"); 102 | 103 | let firstDepartureTime = moment(departuresIn30MinsResult.departures[0].departure).unix(); 104 | if (departuresIn30MinsResult.departures[0].departureDelay) firstDepartureTime += departuresIn30MinsResult.departures[0].departureDelay; 105 | expect(firstDepartureTime).toBeGreaterThanOrEqual(in30Mins - 60); 106 | 107 | // Test journeys 108 | const journeysResult = await client.getJourneys({ 109 | origin: provider.journeyOrigin, 110 | destination: provider.journeyDestination, 111 | via: provider.journeyVia ? [provider.journeyVia] : [], 112 | includeFares: true, 113 | includeSituations: true 114 | }); 115 | 116 | expect(journeysResult.success).toEqual(true); 117 | expect(journeysResult.situations.length).toBeGreaterThanOrEqual(0); 118 | expect(journeysResult.journeys.length).toBeGreaterThanOrEqual(1); 119 | expect(journeysResult.journeys[0].type).toEqual("journey"); 120 | expect(journeysResult.journeys[0]).toHaveProperty("tickets"); 121 | 122 | const journey = journeysResult.journeys[0]; 123 | expect(journey.legs[0].origin.type).toEqual("stop"); 124 | expect(journey.legs[0].origin.id).toEqual(provider.journeyOrigin); 125 | expect(journey.legs[journey.legs.length - 1].destination.type).toEqual("stop"); 126 | expect(journey.legs[journey.legs.length - 1].destination.id).toEqual(provider.journeyDestination); 127 | 128 | let viaIncluded = false; 129 | if (provider.via) { 130 | for (const leg of journey.legs) { 131 | if ((leg.origin.type == "stop" && leg.origin.id == provider.via) || (leg.destination.type == "stop" && leg.destination.id == provider.via)) { 132 | viaIncluded = true; 133 | } 134 | } 135 | } 136 | 137 | expect(viaIncluded).toEqual(provider.via != null); 138 | 139 | }); 140 | } 141 | }); -------------------------------------------------------------------------------- /test/parse.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { parseResponse } = require("../lib/RequestAndParse"); 3 | 4 | const docWithTriasNs = `\ 5 | 6 | 7 | 8 | trias: 9 | _ 10 | 11 | `; 12 | 13 | describe("Test parsing", () => { 14 | it("Should strip TRIAS namespace", () => { 15 | const doc = parseResponse(docWithTriasNs) 16 | const triasEl = doc.children.find(c => c.type === "tag" && c.name === "Trias") 17 | 18 | const childTags = triasEl.children 19 | .filter(c => c.type === "tag") 20 | .map(c => c.name); 21 | expect(childTags).toEqual(["foo", "bar", "baz"]) // strips NS from tag names 22 | 23 | const barEl = triasEl.children.find(c => c.type === "tag" && c.name === "bar") 24 | const barText = barEl.children.find(c => c.type === "text") 25 | expect(barText.data).toBe("trias:") // does not strip text content 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2018", "dom"], 4 | "module": "commonjs", 5 | "target": "es2018", 6 | "declaration": true, 7 | "outDir": "./lib", 8 | "strict": true 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules", "**/__tests__/*"] 12 | } --------------------------------------------------------------------------------