From 895cd525da2de8fc0195ca8f4fbf312f0afba3e0 Mon Sep 17 00:00:00 2001 From: Andreas Hippler Date: Sun, 29 Jul 2018 23:25:42 +0200 Subject: [PATCH] #22 #21 add multiple pin types support, change coordinates to [lng, lat], fix pin connections --- src/coords.ts | 8 +- src/find.ts | 28 +++++-- src/interfaces.ts | 18 ++++- src/lines.ts | 7 +- src/monitor.ts | 8 +- src/pins.ts | 28 ++++--- src/route.ts | 83 ++++----------------- src/test/helper.ts | 52 ++++++++++++- src/test/index.spec.ts | 109 ++++++++++++++------------- src/test/utils.spec.ts | 107 ++++++++++++++++++++++++++- src/utils.ts | 163 ++++++++++++++++++++++++++++++++++------- 11 files changed, 425 insertions(+), 186 deletions(-) diff --git a/src/coords.ts b/src/coords.ts index 540128e..3879811 100755 --- a/src/coords.ts +++ b/src/coords.ts @@ -1,7 +1,11 @@ import axios, { AxiosRequestConfig } from "axios"; import * as utils from "./utils"; -/** Patch section titled API of README.md file in given directory. */ +/** + * Find the coordinates for a given POI id. + * @param id the POI ID + * @returns coordinate as [lng, lat] or undefined + */ export function coords(id: string): Promise { const options: AxiosRequestConfig = { @@ -20,6 +24,6 @@ export function coords(id: string): Promise { const coordinates = response.data.split("|"); - return utils.gk4toWgs84(coordinates[0], coordinates[1]); + return utils.GK4toWGS84(coordinates[1], coordinates[0]); }); } diff --git a/src/find.ts b/src/find.ts index 1fe2ad4..22db9c6 100755 --- a/src/find.ts +++ b/src/find.ts @@ -2,7 +2,7 @@ import axios, { AxiosRequestConfig } from "axios"; import { IAddress, IPoint } from "./interfaces"; import * as utils from "./utils"; -async function pointFinder(name: string, stopsOnly: boolean, assignedStops: boolean): Promise { +function pointFinder(name: string, stopsOnly: boolean, assignedStops: boolean): Promise { if (typeof name !== "string") { throw utils.constructError("ValidationError", "query has to be a string"); } @@ -32,7 +32,7 @@ async function pointFinder(name: string, stopsOnly: boolean, assignedStops: bool const city = poi[2] === "" ? "Dresden" : poi[2]; const idAndType = utils.parsePoiID(poi[0]); - const coords = utils.gk4toWgs84(poi[4], poi[5]); + const coords = utils.GK4toWGS84(poi[5], poi[4]); if (coords) { const point: IPoint = { @@ -52,17 +52,33 @@ async function pointFinder(name: string, stopsOnly: boolean, assignedStops: bool .catch(utils.convertError); } -export async function findStop(searchString: string) { +/** + * Search for a single stop in the network of the DVB. + * @param searchString the name of the stop + * @returns an array of all possible hits including their GPS coordinates. + */ +export function findStop(searchString: string): Promise { return pointFinder(searchString, true, false); } -export async function findPOI(searchString: string) { +/** + * Search for POI in the network of the DVB. + * @param searchString the name of the stop + * @returns an array of all possible hits including their GPS coordinates. + */ +export function findPOI(searchString: string): Promise { return pointFinder(searchString, false, false); } -export async function findAddress(lat: number, lng: number) { +/** + * Lookup address and nearby stops by coordinate. + * @param lng longitude of the coordinate + * @param lat latitude of the coordinate + * @returns the adress and neaby stops + */ +export function findAddress(lng: number, lat: number): Promise { - const gk4 = utils.wgs84toGk4(lat, lng); + const gk4 = utils.WGS84toGK4(lng, lat); return pointFinder(`coord:${gk4[0]}:${gk4[1]}`, false, true) .then((points) => { diff --git a/src/interfaces.ts b/src/interfaces.ts index e7d301a..030f5ea 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -13,32 +13,42 @@ export enum PIN_TYPE { ticketmachine = "ticketmachine", carsharing = "carsharing", parkandride = "parkandride", + unknown = "unknown", } +/** + * WGS84 coordinates [lng, lat] + */ export type coord = number[]; export interface IDiva { number: number; - network: string; + network?: string; } export interface IPlatform { name: string; type: string; } - +/** + * - The id for PIN_TYPE.platform is always an empty string. + * - PIN_TYPE.platform conatins platform_nr. + * - PIN_TYPE.stop contains connections. + * - PIN_TYPE.parkandride contains info. + */ export interface IPin { id: string; type: PIN_TYPE; name: string; - coords?: coord; + coords: coord; platform_nr?: string; connections?: IConnection[]; + info?: string; } export interface IConnection { line: string; - type: string; + mode: IMode; } export interface IMode { diff --git a/src/lines.ts b/src/lines.ts index 9757062..43f598c 100644 --- a/src/lines.ts +++ b/src/lines.ts @@ -14,8 +14,11 @@ function parseLine(line: any): ILine { directions: line.Directions.map(parseDirection), }; } - -export async function lines(stopID: string): Promise { +/** + * get a list of availible tram/bus lines for a stop. + * @param stopID the stop ID + */ +export function lines(stopID: string): Promise { const options: AxiosRequestConfig = { url: "https://webapi.vvo-online.de/stt/lines", diff --git a/src/monitor.ts b/src/monitor.ts index 7f2ebcd..7e349e4 100755 --- a/src/monitor.ts +++ b/src/monitor.ts @@ -2,7 +2,13 @@ import axios, { AxiosRequestConfig } from "axios"; import { IMonitor } from "./interfaces"; import * as utils from "./utils"; -export async function monitor(stopID: string, offset = 0, amount = 0): Promise { +/** + * Monitor a single stop to see every bus or tram leaving this stop after the specified time offset. + * @param stopID ID of the stop + * @param offset how many minutes in the future, 0 for now + * @param amount number of results + */ +export function monitor(stopID: string, offset = 0, amount = 0): Promise { const now = new Date(); const time = new Date(now.getTime() + (offset * 60 * 1000)); diff --git a/src/pins.ts b/src/pins.ts index ba1deb5..441d309 100755 --- a/src/pins.ts +++ b/src/pins.ts @@ -2,21 +2,29 @@ import axios, { AxiosRequestConfig } from "axios"; import { IPin, PIN_TYPE } from "./interfaces"; import * as utils from "./utils"; -export async function pins(swlat: number, swlng: number, nelat: number, nelng: number, - pinType: PIN_TYPE): Promise { +/** + * Search for different kinds of POIs inside a given bounding box. + * @param swlng the longitude of the south west coordinate + * @param swlat the latitude of the south west coordinate + * @param nelng the longitude of the north east coordinate + * @param nelat the latitude of the north east coordinate + * @param pinTypes array of pin types + */ +export function pins(swlng: number, swlat: number, nelng: number, nelat: number, + pinTypes: PIN_TYPE[] = [PIN_TYPE.stop]): Promise { - const sw = utils.wgs84toGk4(swlat, swlng); - const ne = utils.wgs84toGk4(nelat, nelng); + const sw = utils.WGS84toGK4(swlng, swlat); + const ne = utils.WGS84toGK4(nelng, nelat); + let url = "https://www.dvb.de/apps/map/pins?showLines=true"; + pinTypes.forEach((type) => url += `&pintypes=${type}`); const options: AxiosRequestConfig = { - url: "https://www.dvb.de/apps/map/pins", + url, params: { - showLines: "true", - swlat: sw[1], swlng: sw[0], - nelat: ne[1], + swlat: sw[1], nelng: ne[0], - pintypes: pinType, + nelat: ne[1], }, responseType: "text", }; @@ -25,5 +33,5 @@ export async function pins(swlat: number, swlng: number, nelat: number, nelng: n .then((response) => { return response.data || []; }) - .then((elements) => elements.map((elem: string) => utils.parsePin(elem, pinType))); + .then((elements) => elements.map((elem: string) => utils.parsePin(elem))); } diff --git a/src/route.ts b/src/route.ts index ad0f0a8..91165e0 100755 --- a/src/route.ts +++ b/src/route.ts @@ -1,74 +1,19 @@ import axios, { AxiosRequestConfig } from "axios"; -import { ILocation, INode, IRoute, IStop, IStopLocation, ITrip } from "./interfaces"; +import { ILocation, IRoute } from "./interfaces"; import * as utils from "./utils"; -function extractStop(stop: any): IStop { - return { - name: stop.Name.trim(), - city: stop.Place, - type: stop.Type, - platform: utils.parsePlatform(stop.Platform), - coords: utils.gk4toWgs84(stop.Latitude, stop.Longitude) || [0, 0], - arrival: utils.parseDate(stop.ArrivalTime), - departure: utils.parseDate(stop.DepartureTime), - }; -} - -function extractNode(node: any, mapData: any): INode { - const stops: IStop[] = node.RegularStops ? node.RegularStops.map(extractStop) : []; - - let departure: IStopLocation | undefined; - let arrival: IStopLocation | undefined; - - if (stops && stops.length > 1) { - const firstStop = stops[0]; - const lastStop = stops[stops.length - 1]; - - departure = { - name: firstStop.name, - city: firstStop.city, - platform: firstStop.platform, - time: firstStop.departure, - coords: firstStop.coords, - type: firstStop.type, - }; - - arrival = { - name: lastStop.name, - city: lastStop.city, - platform: lastStop.platform, - time: lastStop.arrival, - coords: lastStop.coords, - type: lastStop.type, - }; - } - - return { - stops, - departure, - arrival, - mode: utils.parseMode(node.Mot.Type), - line: node.Mot.Name ? node.Mot.Name : "", - direction: node.Mot.Direction ? node.Mot.Direction.trim() : "", - diva: utils.parseDiva(node.Mot.Diva), - duration: 1, - path: utils.convertCoordinates(mapData[node.MapDataIndex]), - }; -} - -function extractTrip(trip: any): ITrip { - const nodes: INode[] = trip.PartialRoutes.map((node: any) => extractNode(node, trip.MapData)); - - return { - nodes, - departure: nodes[0].departure, - arrival: nodes[nodes.length - 1].arrival, - duration: 1, - interchanges: trip.Interchanges, - }; -} - -export async function route(originID: string, destinationID: string, +/** + * Query the server for possible routes from one stop to another. + * @param originID the id of the origin stop + * @param destinationID the id of the destination stop + * @param time starting at what time + * @param isArrivalTime is time the arrival time + * @returns Returns multiple possible trips, the bus-/tramlines to be taken, + * the single stops, their arrival and departure times and their GPS coordinates. + * The path property of a trip contains an array consisting of all the coordinates + * describing the path of this node. This can be useful to draw the route on a map. + */ +export function route(originID: string, destinationID: string, time = new Date(), isArrivalTime = true): Promise { const options: AxiosRequestConfig = { @@ -90,7 +35,7 @@ export async function route(originID: string, destinationID: string, utils.checkStatus(response.data); if (response.data.Routes) { - const trips = response.data.Routes.map(extractTrip); + const trips = response.data.Routes.map(utils.extractTrip); let origin: ILocation | undefined; let destination: ILocation | undefined; diff --git a/src/test/helper.ts b/src/test/helper.ts index 8883e7e..db8cd46 100644 --- a/src/test/helper.ts +++ b/src/test/helper.ts @@ -1,15 +1,15 @@ import { assert } from "chai"; import { - coord, IAddress, IDiva, ILocation, IMode, - IPlatform, IPoint, IStop, IStopLocation, POI_TYPE, + coord, IAddress, IConnection, IDiva, ILocation, IMode, + IPin, IPlatform, IPoint, IStop, IStopLocation, PIN_TYPE, POI_TYPE, } from "../interfaces"; export function assertCoords(coords: coord) { assert.isArray(coords); assert.lengthOf(coords, 2); - assert.approximately(coords[0], 51, 1); - assert.approximately(coords[1], 13, 2); + assert.approximately(coords[0], 13, 2); + assert.approximately(coords[1], 51, 1); } export function assertPlatform(platform: IPlatform) { @@ -98,3 +98,47 @@ export function assertStopLocation(stop: IStopLocation) { assert.strictEqual(stop.type, POI_TYPE.Stop); } + +export function assertConnection(con: IConnection) { + assert.isObject(con); + assert.isString(con.line); + assert.isNotEmpty(con.line); + assertMode(con.mode); +} + +export function assertPin(pin: IPin, type?: PIN_TYPE) { + assert.isObject(pin); + assert.isString(pin.type); + + if (type) { + assert.strictEqual(pin.type, type); + } + + assert.isString(pin.id); + assert.isString(pin.name); + assertCoords(pin.coords); + + if (pin.type === PIN_TYPE.platform) { + assert.isString(pin.platform_nr); + assert.isNotEmpty(pin.platform_nr); + } else { + assert.isUndefined(pin.platform_nr); + } + + if (pin.type === PIN_TYPE.stop) { + assert.isArray(pin.connections); + if (pin.name !== "Ebertplatz") { + assert.isNotEmpty(pin.connections); + } + pin.connections!.forEach(assertConnection); + } else { + assert.isUndefined(pin.connections); + } + + if (pin.type === PIN_TYPE.parkandride) { + assert.isString(pin.info); + assert.isNotEmpty(pin.info); + } else { + assert.isUndefined(pin.info); + } +} diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index caa6b62..a9e1c71 100755 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -2,8 +2,8 @@ import chai, { assert } from "chai"; import chaiAsPromised from "chai-as-promised"; import * as dvb from "../index"; import { - assertAddress, assertCoords, assertDiva, assertLocation, - assertMode, assertPlatform, assertPoint, assertStop, assertStopLocation, + assertAddress, assertCoords, assertDiva, assertLocation, assertMode, + assertPin, assertPlatform, assertPoint, assertStop, assertStopLocation, } from "./helper"; before(() => { @@ -114,7 +114,7 @@ describe("dvb.route", () => { assert.isNotEmpty(node.path); node.path.forEach(assertCoords); } else { - assert.strictEqual(node.path.length, 0); + assert.isEmpty(node.path); } } @@ -235,87 +235,89 @@ describe("dvb.findPOI", () => { }); describe("dvb.pins", () => { - describe('dvb.pins "51.026578, 13.713899, 51.035565, 13.737974, stop"', () => { - it("should resolve into an array", - () => dvb.pins(51.026578, 13.713899, 51.035565, 13.737974, dvb.PIN_TYPE.stop) + describe('dvb.pins "13.713899, 51.026578, 13.939144, 51.093821, stop"', () => { + it("should contain objects with id, name, coords and connections", + () => dvb.pins(13.713899, 51.026578, 13.939144, 51.093821, [dvb.PIN_TYPE.stop]) .then((data) => { assert.isNotEmpty(data); + data.forEach((pin) => assertPin(pin, dvb.PIN_TYPE.stop)); })); + }); - it("should contain objects with id, name, coords and connections", - () => dvb.pins(51.026578, 13.713899, 51.035565, 13.737974, dvb.PIN_TYPE.stop) + describe('dvb.pins "13.713899, 51.026578, 13.737974, 51.035565, platform"', () => { + it("should contain objects with name, coords and platform_nr", + () => dvb.pins(13.713899, 51.026578, 13.737974, 51.035565, [dvb.PIN_TYPE.platform]) .then((data) => { - data.forEach((elem) => { - assert.isString(elem.id); - assert.isString(elem.name); - assert.isDefined(elem.coords); - assertCoords(elem.coords!); - - assert.isNotEmpty(elem.connections); - assert.isArray(elem.connections); - elem.connections!.forEach((con) => { - assert.isString(con.line); - assert.isString(con.type); - }); - }); + assert.isNotEmpty(data); + data.forEach((pin) => assertPin(pin, dvb.PIN_TYPE.platform)); })); }); - describe('dvb.pins "51.026578, 13.713899, 51.035565, 13.737974, platform"', () => { - it("should contain objects with name, coords and platform_nr", - () => dvb.pins(51.026578, 13.713899, 51.035565, 13.737974, dvb.PIN_TYPE.platform) + describe('dvb.pins "13.713899, 51.026578, 13.737974, 51.035565, POI"', () => { + it("should contain objects with name, coords and id", + () => dvb.pins(13.713899, 51.026578, 13.737974, 51.035565, [dvb.PIN_TYPE.poi]) .then((data) => { - assert.notEqual(0, data.length); - data.forEach((elem) => { - assert.isString(elem.name); - assert.isDefined(elem.coords); - assertCoords(elem.coords!); - assert.isString(elem.platform_nr); - }); + assert.isNotEmpty(data); + data.forEach((pin) => assertPin(pin, dvb.PIN_TYPE.poi)); })); }); - describe('dvb.pins "51.026578, 13.713899, 51.035565, 13.737974, POI"', () => { - it("should contain objects with name, coords and id", - () => dvb.pins(51.026578, 13.713899, 51.035565, 13.737974, dvb.PIN_TYPE.poi) + describe("multiple pin types", () => { + it("should contain ticketmachine and platform", + () => dvb.pins(13.713899, 51.026578, 13.737974, 51.035565, [dvb.PIN_TYPE.platform, dvb.PIN_TYPE.ticketmachine]) .then((data) => { - assert.notEqual(0, data.length); - data.forEach((elem) => { - assert.isString(elem.id); - assert.isString(elem.name); - assert.isDefined(elem.coords); - assertCoords(elem.coords!); - }); + assert.isNotEmpty(data); + data.forEach((pin) => assertPin(pin)); + const platform = data.filter((pin) => pin.type === dvb.PIN_TYPE.platform); + const ticketmachine = data.filter((pin) => pin.type === dvb.PIN_TYPE.ticketmachine); + assert.isNotEmpty(platform); + assert.isNotEmpty(ticketmachine); + assert.strictEqual(platform.length + ticketmachine.length, data.length); + })); + + it("should contain poi, ticketmachine and stop", + () => dvb.pins(13.713899, 51.026578, 13.737974, 51.035565, + [dvb.PIN_TYPE.poi, dvb.PIN_TYPE.ticketmachine, dvb.PIN_TYPE.stop]) + .then((data) => { + assert.isNotEmpty(data); + data.forEach((pin) => assertPin(pin)); + const poi = data.filter((pin) => pin.type === dvb.PIN_TYPE.poi); + const ticketmachine = data.filter((pin) => pin.type === dvb.PIN_TYPE.ticketmachine); + const stop = data.filter((pin) => pin.type === dvb.PIN_TYPE.stop); + assert.isNotEmpty(poi); + assert.isNotEmpty(ticketmachine); + assert.isNotEmpty(stop); + assert.strictEqual(poi.length + ticketmachine.length + stop.length, data.length); })); }); describe('dvb.pins "0, 0, 0, 0, stop"', () => { it("should resolve into an empty array", - () => dvb.pins(0, 0, 0, 0, dvb.PIN_TYPE.stop) + () => dvb.pins(0, 0, 0, 0) .then((data) => { assert.isArray(data); - assert.lengthOf(data, 0); + assert.isEmpty(data); })); }); }); describe("dvb.findAddress", () => { - describe('dvb.findAddress "51.025451, 13.722943"', () => { + describe('dvb.findAddress "13.722943, 51.025451"', () => { const lat = 51.025451; const lng = 13.722943; it("should resolve into an object with city, address and coords properties", - () => dvb.findAddress(lat, lng) + () => dvb.findAddress(lng, lat) .then((address) => { assert.isDefined(address); assert.strictEqual(address!.name, "Nöthnitzer Straße 46"); assert.strictEqual(address!.city, "Dresden"); assert.strictEqual(address!.type, dvb.POI_TYPE.Coords); - assert.approximately(address!.coords[0], lat, 0.001); - assert.approximately(address!.coords[1], lng, 0.001); + assert.approximately(address!.coords[0], lng, 0.001); + assert.approximately(address!.coords[1], lat, 0.001); })); - it("should contain nearby stops", () => dvb.findAddress(lat, lng) + it("should contain nearby stops", () => dvb.findAddress(lng, lat) .then((address) => { assert.isDefined(address); assertAddress(address!); @@ -331,9 +333,8 @@ describe("dvb.findAddress", () => { describe("dvb.coords", () => { describe('dvb.coords "33000755"', () => { - it("should resolve into a coordinate array [lat, lng]", () => dvb.coords("33000755") + it("should resolve into a coordinate array [lng, lat]", () => dvb.coords("33000755") .then((data) => { - assert.isDefined(data); assertCoords(data!); })); }); @@ -348,14 +349,10 @@ describe("dvb.coords", () => { describe("dvb.coords for id from dvb.pins", () => { it("coordinates should be equal for first pin", - () => dvb.pins(51.026578, 13.713899, 51.035565, 13.737974, dvb.PIN_TYPE.poi) + () => dvb.pins(13.713899, 51.026578, 13.737974, 51.035565, [dvb.PIN_TYPE.poi]) .then((pins) => { assert.isNotEmpty(pins); - pins.forEach((elem) => { - assert.isString(elem.id); - assert.isDefined(elem.coords); - assertCoords(elem.coords!); - }); + pins.forEach((pin) => assertPin(pin, dvb.PIN_TYPE.poi)); return dvb.coords(pins[0].id) .then((coords) => { diff --git a/src/test/utils.spec.ts b/src/test/utils.spec.ts index 228027d..8d61d53 100644 --- a/src/test/utils.spec.ts +++ b/src/test/utils.spec.ts @@ -1,14 +1,15 @@ import { assert } from "chai"; +import { PIN_TYPE } from "../interfaces"; import * as utils from "../utils"; -import { assertMode } from "./helper"; +import { assertCoords, assertMode, assertPin } from "./helper"; describe("internal utils", () => { describe("parseMode", () => { const mots = [ ["Tram", "Tram"], - ["Bus", "Bus"], - ["Citybus", "Bus"], - ["Intercitybus", "Bus"], + ["Bus", "CityBus"], + ["Citybus", "CityBus"], + ["Intercitybus", "IntercityBus"], ["Suburbanrailway", "SuburbanRailway"], ["Train", "Train"], ["Rapidtransit", "Train"], @@ -89,5 +90,103 @@ describe("internal utils", () => { assert.strictEqual(error.message, "bar"); } }); + + it("should throw passed error", () => { + const error = new Error("400 - foo: bar"); + assert.throws(() => utils.convertError(error), Error); + }); + }); + + describe("transform coords", () => { + const wgs84 = [13.722766, 51.025835]; + const gk4 = [4620969, 5655929]; + + it("WGS84toGK4", () => { + const point = utils.WGS84toGK4(wgs84[0], wgs84[1]); + assert.approximately(point[0], gk4[0], 3); + assert.approximately(point[1], gk4[1], 3); + }); + + it("GK4toWGS84", () => { + const point = utils.GK4toWGS84(gk4[0] + "", gk4[1] + ""); + assert.approximately(point![0], wgs84[0], 0.0001); + assert.approximately(point![1], wgs84[1], 0.0001); + }); + + it("GK4toWGS84 should return undefined", () => { + let point = utils.GK4toWGS84("", ""); + assert.isUndefined(point); + + point = utils.GK4toWGS84("0", "0"); + assert.isUndefined(point); + }); + + it("convertCoordinates", () => { + const points = utils.convertCoordinates(`${gk4[0]}|${gk4[1]}|${gk4[0]}|${gk4[1]}|`); + assert.isNotEmpty(points); + points.forEach(assertCoords); + }); + }); + + it("parseDate", () => { + const date = utils.parseDate("/Date(1532818920000-0000)/"); + assert.typeOf(date, "date"); + assert.strictEqual(date.getTime(), 1532818920000); + }); + + describe("parsePin", () => { + it("stop", () => { + const pin = utils.parsePin("33000028|||Hauptbahnhof|5657516|4621644||1:3~6~7~8~9~10~11#2:66~H/S#3:261~333~352~360" + + "~366~400~424~672~Fernbus#4:EC~IC~ICE~RB~RE~TL~TLX#5:S1~S2~S3"); + assertPin(pin, PIN_TYPE.stop); + assert.strictEqual(pin.id, "33000028"); + assert.strictEqual(pin.name, "Hauptbahnhof"); + assert.lengthOf(pin.connections!, 28); + }); + + it("platform", () => { + const pin = utils.parsePin("|pf||Nürnberger Platz|5656555|4621180|1|"); + assertPin(pin, PIN_TYPE.platform); + assert.strictEqual(pin.id, ""); + assert.strictEqual(pin.name, "Nürnberger Platz"); + assert.strictEqual(pin.platform_nr, "1"); + }); + + it("poi", () => { + const pin = utils.parsePin("poiID:2104107042:14612000:|p||Helmholtz-Apotheke|5656699|4621216||"); + assertPin(pin, PIN_TYPE.poi); + }); + + it("rentabike", () => { + const pin = utils.parsePin("poiID:2104108009:14612000:|r||SZ-Bike Station - Nürnberger Platz|5656570|4621200||"); + assertPin(pin, PIN_TYPE.rentabike); + assert.strictEqual(pin.id, "poiID:2104108009:14612000:"); + assert.strictEqual(pin.name, "SZ-Bike Station - Nürnberger Platz"); + }); + + it("ticketmachine", () => { + const pin = utils.parsePin( + "poiID:2104108217:14612000:|t||Ticketautomat Dresden, Fetscherplatz -|5658310|4624283||"); + assertPin(pin, PIN_TYPE.ticketmachine); + assert.strictEqual(pin.id, "poiID:2104108217:14612000:"); + assert.strictEqual(pin.name, "Ticketautomat Dresden, Fetscherplatz -"); + }); + + it("carsharing", () => { + const pin = utils.parsePin( + "poiID:2104108159:14612000:|c||teilAuto Station Stresemannplatz - (STR)|5657940|4624508||"); + assertPin(pin, PIN_TYPE.carsharing); + assert.strictEqual(pin.id, "poiID:2104108159:14612000:"); + assert.strictEqual(pin.name, "teilAuto Station Stresemannplatz - (STR)"); + }); + + it("parkandride", () => { + const pin = utils.parsePin( + "poiID:2104107859:14612000:|pr||P+R Dresden Reick|5655506|4625718|21 Stellplätze, kostenfrei, 24h|"); + assertPin(pin, PIN_TYPE.parkandride); + assert.strictEqual(pin.id, "poiID:2104107859:14612000:"); + assert.strictEqual(pin.name, "P+R Dresden Reick"); + assert.strictEqual(pin.info, "21 Stellplätze, kostenfrei, 24h"); + }); }); }); diff --git a/src/utils.ts b/src/utils.ts index ce16b03..4812a97 100755 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,25 +1,19 @@ import proj4 from "proj4"; import { - coord, - IConnection, - IDiva, - IMode, - IPin, - IPlatform, - PIN_TYPE, - POI_TYPE, + coord, IConnection, IDiva, IMode, INode, IPin, IPlatform, + IStop, IStopLocation, ITrip, PIN_TYPE, POI_TYPE, } from "./interfaces"; proj4.defs( - "GK4", + "EPSG:31468", "+proj=tmerc +lat_0=0 +lon_0=12 +k=1 +x_0=4500000 +y_0=0 +ellps=bessel +datum=potsdam +units=m +no_defs", ); -export function wgs84toGk4(lat: number, lng: number): coord { - return proj4("GK4").forward([Number(lng), Number(lat)]).map(Math.round); +export function WGS84toGK4(lng: number, lat: number): coord { + return proj4("EPSG:31468").forward([lng, lat]).map(Math.round); } -export function gk4toWgs84(lat: string, lng: string): coord | undefined { +export function GK4toWGS84(lng: string, lat: string): coord | undefined { const latInt = parseInt(lat, 10); const lngInt = parseInt(lng, 10); @@ -31,7 +25,7 @@ export function gk4toWgs84(lat: string, lng: string): coord | undefined { return undefined; } - return proj4("GK4").inverse([lngInt, latInt]).reverse(); + return proj4("EPSG:31468").inverse([lngInt, latInt]); } export function convertCoordinates(s: string): coord[] { @@ -43,7 +37,7 @@ export function convertCoordinates(s: string): coord[] { let i = 1; const len = gk4Chords.length - 1; while (i < len) { - const coordinate = gk4toWgs84(gk4Chords[i], gk4Chords[i + 1]); + const coordinate = GK4toWGS84(gk4Chords[i + 1], gk4Chords[i]); if (coordinate) { coords.push(coordinate); } @@ -93,27 +87,51 @@ export function parsePlatform(p?: any): IPlatform | undefined { return p ? { name: p.Name, type: p.Type } : undefined; } -export function parsePin(dataAsString: string, pinType: PIN_TYPE): IPin { +function pinType(str: string): PIN_TYPE { + switch (str) { + case "": return PIN_TYPE.stop; + case "p": return PIN_TYPE.poi; + case "pf": return PIN_TYPE.platform; + case "pr": return PIN_TYPE.parkandride; + case "r": return PIN_TYPE.rentabike; + case "c": return PIN_TYPE.carsharing; + case "t": return PIN_TYPE.ticketmachine; + } + return PIN_TYPE.unknown; +} + +export function parsePin(dataAsString: string): IPin { const data = dataAsString.split("|"); - const coords = gk4toWgs84(data[4], data[5]); + const coords = GK4toWGS84(data[5], data[4]) || []; - if (pinType === PIN_TYPE.platform) { + const type = pinType(data[1]); + + if (type === PIN_TYPE.platform) { return { coords, - id: "//TODO", + id: data[0], name: data[3], platform_nr: data[6], - type: pinType, + type, + }; + } + if (type === PIN_TYPE.poi || type === PIN_TYPE.rentabike || type === PIN_TYPE.ticketmachine + || type === PIN_TYPE.carsharing || type === PIN_TYPE.unknown) { + return { + coords, + id: data[0], + name: data[3], + type, }; } - if (pinType === PIN_TYPE.poi || pinType === PIN_TYPE.rentabike - || pinType === PIN_TYPE.ticketmachine || pinType === PIN_TYPE.carsharing - || pinType === PIN_TYPE.parkandride) { + + if (type === PIN_TYPE.parkandride) { return { coords, id: data[0], name: data[3], - type: pinType, + info: data[6], + type, }; } @@ -123,7 +141,7 @@ export function parsePin(dataAsString: string, pinType: PIN_TYPE): IPin { id: data[0], name: data[3], connections: parseConnections(data[7]), - type: pinType, + type, }; } @@ -133,8 +151,9 @@ export function parseMode(name: string): IMode { return MODES.Tram; case "bus": case "citybus": + return MODES.CityBus; case "intercitybus": - return MODES.Bus; + return MODES.IntercityBus; case "suburbanrailway": return MODES.SuburbanRailway; case "train": @@ -201,29 +220,117 @@ export function parsePoiID(id: string) { }; } +function connectionType(str: string): IMode | undefined { + switch (str) { + case "1": return MODES.Tram; + case "2": return MODES.CityBus; + case "3": return MODES.IntercityBus; + case "4": return MODES.Train; + case "5": return MODES.SuburbanRailway; + case "6": return MODES.HailedSharedTaxi; + case "7": return MODES.Ferry; + case "8": return MODES.Cableway; + } +} + function parseConnections(data: string): IConnection[] { let connections: IConnection[] = []; data.split("#").forEach((types) => { + if (!types) { + return []; + } const typesArray = types.split(":"); + const mode = connectionType(typesArray[0]) as IMode; connections = connections.concat(typesArray[1].split("~").map((line) => ({ line, - type: typesArray[0], + mode, }))); }); return connections; } +function extractStop(stop: any): IStop { + return { + name: stop.Name.trim(), + city: stop.Place, + type: stop.Type, + platform: parsePlatform(stop.Platform), + coords: GK4toWGS84(stop.Longitude, stop.Latitude) || [0, 0], + arrival: parseDate(stop.ArrivalTime), + departure: parseDate(stop.DepartureTime), + }; +} + +function extractNode(node: any, mapData: any): INode { + const stops: IStop[] = node.RegularStops ? node.RegularStops.map(extractStop) : []; + + let departure: IStopLocation | undefined; + let arrival: IStopLocation | undefined; + + if (stops && stops.length > 1) { + const firstStop = stops[0]; + const lastStop = stops[stops.length - 1]; + + departure = { + name: firstStop.name, + city: firstStop.city, + platform: firstStop.platform, + time: firstStop.departure, + coords: firstStop.coords, + type: firstStop.type, + }; + + arrival = { + name: lastStop.name, + city: lastStop.city, + platform: lastStop.platform, + time: lastStop.arrival, + coords: lastStop.coords, + type: lastStop.type, + }; + } + + return { + stops, + departure, + arrival, + mode: parseMode(node.Mot.Type), + line: node.Mot.Name ? node.Mot.Name : "", + direction: node.Mot.Direction ? node.Mot.Direction.trim() : "", + diva: parseDiva(node.Mot.Diva), + duration: 1, + path: convertCoordinates(mapData[node.MapDataIndex]), + }; +} + +export function extractTrip(trip: any): ITrip { + const nodes: INode[] = trip.PartialRoutes.map((node: any) => extractNode(node, trip.MapData)); + + return { + nodes, + departure: nodes[0].departure, + arrival: nodes[nodes.length - 1].arrival, + duration: 1, + interchanges: trip.Interchanges, + }; +} + export const MODES = { Tram: { title: "Straßenbahn", name: "Tram", icon_url: "https://www.dvb.de/assets/img/trans-icon/transport-tram.svg", }, - Bus: { + CityBus: { title: "Bus", - name: "Bus", + name: "CityBus", + icon_url: "https://www.dvb.de/assets/img/trans-icon/transport-bus.svg", + }, + IntercityBus: { + title: "Regio-Bus", + name: "IntercityBus", icon_url: "https://www.dvb.de/assets/img/trans-icon/transport-bus.svg", }, SuburbanRailway: {