ctucx.git: oeffisearch

[nimlang] fast and simple tripplanner

commit 16580bc85660a9a7a6811f9dce81ee5a6f2eaec3
parent 2ff5e8936bdd6b64b8c41e8337e3eb1dcef001f5
Author: Leah (ctucx) <leah@ctu.cx>
Date: Mon, 8 Mar 2021 12:45:22 +0100

use submodule for hafas-client
27 files changed, 10 insertions(+), 982 deletions(-)
A
.gitmodules
|
3
+++
D
src/backend/hafas.nim
|
7
-------
D
src/backend/hafas/api/journeys.nim
|
79
-------------------------------------------------------------------------------
D
src/backend/hafas/api/refresh_journey.nim
|
23
-----------------------
D
src/backend/hafas/api/suggestions.nim
|
29
-----------------------------
D
src/backend/hafas/errors.nim
|
199
-------------------------------------------------------------------------------
D
src/backend/hafas/parse/accessibility.nim
|
6
------
D
src/backend/hafas/parse/date.nim
|
31
-------------------------------
D
src/backend/hafas/parse/journey.nim
|
38
--------------------------------------
D
src/backend/hafas/parse/journeys_response.nim
|
27
---------------------------
D
src/backend/hafas/parse/leg.nim
|
70
----------------------------------------------------------------------
D
src/backend/hafas/parse/line.nim
|
71
-----------------------------------------------------------------------
D
src/backend/hafas/parse/msg.nim
|
12
------------
D
src/backend/hafas/parse/operator.nim
|
9
---------
D
src/backend/hafas/parse/point.nim
|
78
------------------------------------------------------------------------------
D
src/backend/hafas/parse/polyline.nim
|
73
-------------------------------------------------------------------------
D
src/backend/hafas/parse/products.nim
|
37
-------------------------------------
D
src/backend/hafas/parse/remark.nim
|
32
--------------------------------
D
src/backend/hafas/parse/stopover.nim
|
33
---------------------------------
D
src/backend/hafas/todo
|
10
----------
D
src/backend/hafas/types.nim
|
53
-----------------------------------------------------
D
src/backend/hafas/util.nim
|
57
---------------------------------------------------------
M
src/endpoints/journeys.nim
|
2
+-
M
src/endpoints/moreJourneys.nim
|
5
++---
M
src/endpoints/refreshJourney.nim
|
5
++---
M
src/endpoints/suggestions.nim
|
2
+-
A
src/hafas
|
1
+
diff --git a/.gitmodules b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "src/hafas"]
+	path = src/hafas
+	url = https://cgit.ctu.cx/hafas.nim
diff --git a/src/backend/hafas.nim b/src/backend/hafas.nim
@@ -1,7 +0,0 @@
-import hafas/api/journeys
-import hafas/api/suggestions
-import hafas/api/refresh_journey
-
-export journeys
-export suggestions
-export refresh_journey
diff --git a/src/backend/hafas/api/journeys.nim b/src/backend/hafas/api/journeys.nim
@@ -1,79 +0,0 @@
-import ../parse/products
-import ../parse/point
-import ../parse/accessibility
-import ../parse/journeys_response
-import ../types
-import ../util
-import json
-import times
-import asyncdispatch
-import options
-
-proc journeys*(params: JourneysParams): Future[JourneysResponse] {.async.} =
-  var `when` = now().toTime()
-  var isDeparture = true
-  if params.departure.isSome:
-    `when` = params.departure.get.fromUnix
-  elif params.arrival.isSome:
-    `when` = params.arrival.get.fromUnix
-    isDeparture = false
-
-  let req = %* {
-    "cfg": {
-      "polyEnc": "GPA"
-    },
-    "meth": "TripSearch",
-    "req": {
-      "ctxScr": nil,
-      "getPasslist": params.stopovers.get(false),
-      "maxChg": params.transfers.get(-1),
-      "minChgTime": params.transferTime.get(0),
-      "numF": params.results.get(5),
-      "depLocL": [ params.fromPoint.formatPoint() ],
-      "viaLocL": [],
-      "arrLocL": [ params.toPoint.formatPoint() ],
-      "jnyFltrL": [
-        {
-          "type": "PROD",
-          "mode": "INC",
-          "value": $formatProducts(params.products.get(parseProducts(1023))),
-        },
-        {
-          "type": "META",
-          "mode": "INC",
-          "meta": formatAccessibility(params.accessibility.get(Accessibility.none)),
-        }
-      ],
-      "gisFltrL": [],
-      "getTariff": params.tickets.get(true),
-      "ushrp": params.startWithWalking.get(true),
-      "getPT": true,
-      "getIV": false,
-      "getPolyline": params.polylines.get(false),
-      "outFrwd": isDeparture,
-      "outDate": `when`.format("yyyyMMdd"),
-      "outTime": `when`.format("HHmmss"),
-      "trfReq": {
-        "jnyCl": 2,
-        "tvlrProf": [
-          {
-            "type": "E",
-            "redtnCard": nil
-          }
-        ],
-        "cType": "PK"
-      }
-    }
-  }
-
-  if params.laterRef.isSome:
-    req["req"]["ctxScr"] = %* params.laterRef.get
-  elif params.earlierRef.isSome:
-    req["req"]["ctxScr"] = %* params.earlierRef.get
-
-  if params.viaPoint.isSome:
-    let viaPoint = params.viaPoint.get
-    req["req"]["viaLocL"] = %* [{ "loc": viaPoint.formatPoint() }]
-
-  let data = await request(req)
-  return parseJourneysResponse(data)
diff --git a/src/backend/hafas/api/refresh_journey.nim b/src/backend/hafas/api/refresh_journey.nim
@@ -1,23 +0,0 @@
-import ../types
-import ../parse/journeys_response
-import ../util
-import json
-import asyncdispatch
-import options
-
-proc refreshJourney*(params: RefreshJourneyParams): Future[Journey] {.async.} =
-  let req = %* {
-    "cfg": {
-    },
-    "meth": "Reconstruction",
-    "req": {
-      "ctxRecon": params.refreshToken,
-      "getIST": true,
-      "getPasslist": params.stopovers.get(false),
-      "getPolyline": params.polylines.get(false),
-      "getTariff": params.tickets.get(false),
-    }
-  }
-
-  let data = await request(req)
-  return parseJourneysResponse(data, true).journeys[0]
diff --git a/src/backend/hafas/api/suggestions.nim b/src/backend/hafas/api/suggestions.nim
@@ -1,29 +0,0 @@
-import ../../../types
-import ../parse/point
-import ../util
-import json
-import asyncdispatch
-import sequtils
-import options
-
-proc suggestions*(params: SuggestionsParams): Future[seq[Point]] {.async.} =
-  let req = %* {
-    "cfg": {
-      "polyEnc": "GPA"
-    },
-    "meth": "LocMatch",
-    "req": {
-      "input": {
-        "loc": {
-          "type": "ALL",
-          "name": params.query & "?"
-        },
-        "maxLoc": params.results.get(10),
-        "field": "S"
-      }
-    }
-  }
-  let data = await request(req)
-  let locs = data["res"]["match"]["locL"].getElems()
-
-  return locs.map(parsePoint)
diff --git a/src/backend/hafas/errors.nim b/src/backend/hafas/errors.nim
@@ -1,199 +0,0 @@
-import asynchttpserver
-import ../../types
-
-proc parseError*(errstr: string): hafasException =
-  case errstr:
-  of "H_UNKNOWN":
-    return hafasException(
-      code: SERVER_ERROR,
-      message: "unknown internal error",
-      statusCode: Http500,
-    )
-  of "AUTH":
-    return hafasException(
-      code: ACCESS_DENIED,
-      message: "invalid or missing authentication data",
-      statusCode: Http401,
-    )
-  of "R0001":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "unknown method",
-      statusCode: Http400,
-    )
-  of "R0002":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "invalid or missing request parameters",
-      statusCode: Http400,
-    )
-  of "R0007":
-    return hafasException(
-      code: SERVER_ERROR,
-      message: "internal communication error",
-      statusCode: Http500,
-    )
-  of "R5000":
-    return hafasException(
-      code: ACCESS_DENIED,
-      message: "access denied",
-      statusCode: Http401,
-    )
-  of "S1":
-    return hafasException(
-      code: SERVER_ERROR,
-      message: "journeys search: a connection to the backend server couldn\'t be established",
-      statusCode: Http503,
-    )
-  of "LOCATION":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "location/stop not found",
-      statusCode: Http400,
-    )
-  of "H390":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "journeys search: departure/arrival station replaced",
-      statusCode: Http400,
-    )
-  of "H410":
-    return hafasException(
-      code: SERVER_ERROR,
-      message: "journeys search: incomplete response due to timetable change"
-    )
-  of "H455":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "journeys search: prolonged stop",
-      statusCode: Http400,
-    )
-  of "H460":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "journeys search: stop(s) passed multiple times",
-      statusCode: Http400,
-    )
-  of "H500":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "journeys search: too many trains, connection is not complete",
-      statusCode: Http400,
-    )
-  of "H890":
-    return hafasException(
-      code: NOT_FOUND,
-      message: "journeys search unsuccessful",
-      statusCode: Http404,
-    )
-  of "H891":
-    return hafasException(
-      code: NOT_FOUND,
-      message: "journeys search: no route found, try with an intermediate stations",
-      statusCode: Http404,
-    )
-  of "H892":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "journeys search: query too complex, try less intermediate stations",
-      statusCode: Http400,
-    )
-  of "H895":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "journeys search: departure & arrival are too near",
-      statusCode: Http400,
-    )
-  of "H899":
-    return hafasException(
-      code: SERVER_ERROR,
-      message: "journeys search unsuccessful or incomplete due to timetable change"
-    )
-  of "H900":
-    return hafasException(
-      code: SERVER_ERROR,
-      message: "journeys search unsuccessful or incomplete due to timetable change"
-    )
-  of "H9220":
-    return hafasException(
-      code: NOT_FOUND,
-      message: "journeys search: no stations found close to the address",
-      statusCode: Http400,
-    )
-  of "H9230":
-    return hafasException(
-      code: SERVER_ERROR,
-      message: "journeys search: an internal error occured",
-      statusCode: Http500,
-    )
-  of "H9240":
-    return hafasException(
-      code: NOT_FOUND,
-      message: "journeys search unsuccessful",
-      statusCode: Http404,
-    )
-  of "H9250":
-    return hafasException(
-      code: SERVER_ERROR,
-      message: "journeys search: leg query interrupted",
-      statusCode: Http500,
-    )
-  of "H9260":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "journeys search: unknown departure station",
-      statusCode: Http400,
-    )
-  of "H9280":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "journeys search: unknown intermediate station",
-      statusCode: Http400,
-    )
-  of "H9300":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "journeys search: unknown arrival station",
-      statusCode: Http400,
-    )
-  of "H9320":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "journeys search: the input is incorrect or incomplete",
-      statusCode: Http400,
-    )
-  of "H9360":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "journeys search: error in a data field",
-      statusCode: Http400,
-    )
-  of "H9380":
-    return hafasException(
-      code: INVALID_REQUEST,
-      message: "journeys search: departure/arrival/intermediate station defined more than once",
-      statusCode: Http400,
-    )
-  of "SQ001":
-    return hafasException(
-      code: SERVER_ERROR,
-      message: "no departures/arrivals data available",
-      statusCode: Http503,
-    )
-  of "SQ005":
-    return hafasException(
-      code: NOT_FOUND,
-      message: "no trips found",
-      statusCode: Http404,
-    )
-  of "TI001":
-    return hafasException(
-      code: SERVER_ERROR,
-      message: "no trip info available",
-      statusCode: Http503,
-    )
-  return hafasException(
-    code: SERVER_ERROR,
-    message: "unknown HAFAS exception " & errstr,
-    statusCode: Http500,
-  )
diff --git a/src/backend/hafas/parse/accessibility.nim b/src/backend/hafas/parse/accessibility.nim
@@ -1,6 +0,0 @@
-import ../types
-
-proc formatAccessibility*(a: Accessibility): string =
-  if   a == none:     result = "notBarrierfree"
-  elif a == partial:  result = "limitedBarrierfree"
-  elif a == complete: result = "completeBarrierfree"
diff --git a/src/backend/hafas/parse/date.nim b/src/backend/hafas/parse/date.nim
@@ -1,31 +0,0 @@
-import ../types
-import options
-import strutils
-import times
-
-proc parseDate*(common: CommonData, time: Option[string], tzoffset: Option[int]): Option[int64] =
-  if time.isNone:
-    return none(int64)
-
-  let tzoffset = tzoffset.get(60) # FIXME: sometimes no time zone is given. how to deal with that?
-  let date = common.dateStr
-  var time = time.get
-  var dayoffset = 0
-
-  if time.len == 8:
-    dayoffset = parseInt(time[0..1])
-    time = time[2..7]
-
-  var tzoffhours = align($(int(tzoffset / 60)), 2, '0')
-  var tzoffmins = align($(tzoffset mod 60), 2, '0')
-  var tzoff = tzoffhours & ":" & tzoffmins
-  if tzoffset >= 0:
-    tzoff = "+" & tzoff
-
-  let datestr = date & time & tzoff
-  let dateformat = "yyyyMMddHHmmsszzz"
-  var dt = datestr.parse(dateformat)
-
-  dt = dt + initTimeInterval(days = dayoffset)
-  return some(dt.toTime().toUnix())
-
diff --git a/src/backend/hafas/parse/journey.nim b/src/backend/hafas/parse/journey.nim
@@ -1,38 +0,0 @@
-import ../types
-import json
-import leg
-import options
-
-proc mkParseJourney*(common: CommonData): proc =
-  proc parseJourney(j: JsonNode): Journey =
-    var common = common
-    common.dateStr = j{"date"}.getStr()
-
-    if j{"trfRes"}{"statusCode"}.getStr == "OK":
-      result.price = some(Price(
-        amount: j["trfRes"]["fareSetL"][0]["fareL"][0]["prc"].getInt / 100,
-        currency: some("Euro"),
-      ))
-
-    result.refreshToken = j{"ctxRecon"}.getStr()
-    result.legs = j{"secL"}.getElems().map(mkParseLeg(common))
-    result.lastUpdated = common.timeStamp
-
-    # combine walking legs
-    var i = -1
-    var firstWalking = -1
-    while true:
-      inc(i)
-      if i >= len(result.legs): break
-      if result.legs[i].isWalking:
-        if firstWalking == -1:
-          firstWalking = i
-        else:
-          result.legs[firstWalking].arrival = result.legs[i].arrival
-          result.legs[firstWalking].distance.get += result.legs[i].distance.get
-          result.legs.delete(i)
-          dec(i)
-      else:
-        firstWalking = -1
-
-  return parseJourney
diff --git a/src/backend/hafas/parse/journeys_response.nim b/src/backend/hafas/parse/journeys_response.nim
@@ -1,27 +0,0 @@
-import ../types
-import ./remark
-import ./point
-import ./operator
-import ./journey
-import ./line
-import ./polyline
-import json
-import sequtils
-import strutils
-
-proc parseJourneysResponse*(data: JsonNode, isRefresh: bool = false): JourneysResponse =
-  let points = map(data["res"]["common"]["locL"].getElems(), parsePoint)
-  let operators = map(data["res"]["common"]["opL"].getElems(), parseOperator)
-  let remarks = map(data["res"]["common"]["remL"].getElems(), parseRemark)
-  let lines = data["res"]["common"]["prodL"]
-  let polylines = map(data["res"]["common"]["polyL"].getElems(), mkParsePolyline(points))
-  let timestamp = parseInt(data["res"]["planrtTS"].getStr())
-  let common = CommonData(points: points, operators: operators, remarks: remarks, lines: lines, polylines: polylines, timestamp: timestamp)
-
-  result.journeys = data["res"]["outConL"].getElems().map(mkParseJourney(common))
-  if not isRefresh:
-    if data["res"].hasKey("outCtxScrB"):
-      result.earlierRef = data["res"]["outCtxScrB"].getStr()
-    
-    if data["res"].hasKey("outCtxScrF"):
-      result.laterRef = data["res"]["outCtxScrF"].getStr()
diff --git a/src/backend/hafas/parse/leg.nim b/src/backend/hafas/parse/leg.nim
@@ -1,70 +0,0 @@
-import ../types
-import ./stopover
-import ./msg
-import ./date
-import ./line
-import json
-import options
-
-proc parseLegPart(common: CommonData, lp: JsonNode): LegPart =
-  let h                    = lp.to(HafasStopParams)
-  let plannedDepartureTime = parseDate(common, h.dTimeS, h.dTZOffset)
-  let plannedArrivalTime   = parseDate(common, h.aTimeS, h.aTZOffset)
-
-  if h.dPlatfS.isSome: result.plannedPlatform     = h.dPlatfS
-  elif h.aPlatfS.isSome: result.plannedPlatform   = h.aPlatfS
-  if h.dPlatfR.isSome: result.prognosedPlatform   = h.dPlatfR
-  elif h.aPlatfR.isSome: result.prognosedPlatform = h.aPlatfR
-  if h.dTimeR.isSome: result.prognosedTime        = parseDate(common, h.dTimeR, h.dTZOffset)
-  elif h.aTimeR.isSome: result.prognosedTime      = parseDate(common, h.aTimeR, h.aTZOffset)
-
-  if plannedDepartureTime.isSome: result.plannedTime = plannedDepartureTime.get
-  elif plannedArrivalTime.isSome: result.plannedTime = plannedArrivalTime.get
-  else: raise newException(CatchableError, "missing departure and arrival time for leg")
-
-  result.point = common.points[h.locX.get]
-
-proc mkParseLeg*(common: CommonData): proc =
-  proc parseLeg(l: JsonNode): Leg =
-
-    if l{"jny"}{"polyG"}{"polyXL"}.getElems().len() > 0:
-      result.polyline = some(Polyline(
-        type: "FeatureCollection",
-      ))
-      for n in l{"jny"}{"polyG"}{"polyXL"}.getElems():
-        result.polyline.get.features &= common.polylines[n.getInt()].features
-
-    let typeStr = l{"type"}.getStr()
-
-    echo typeStr
-
-    if typeStr == "JNY":
-      result.direction = some(l{"jny"}{"dirTxt"}.getStr())
-      result.tripId = some(l{"jny"}{"jid"}.getStr())
-      result.line = common.parseLine(l["jny"]["prodX"].getInt())
-
-      let stopovers = l{"jny"}{"stopL"}.getElems()
-      if stopovers.len > 0:
-        result.stopovers = some(stopovers.map(mkParseStopover(common)))
-
-      let remarks = l{"jny"}{"msgL"}.getElems()
-      if remarks.len > 0:
-        result.remarks = some(remarks.map(mkParseMsg(common)))
-
-    elif typeStr == "WALK":
-      result.isWalking = true
-      result.distance = some(l{"gis"}{"dist"}.getInt())
-
-    elif typeStr == "TRSF" or typeStr == "DEVI":
-      result.isTransfer = true
-
-    else:
-      raise newException(CatchableError, "Unimplemented hafas leg type: " & typeStr)
-
-    result.departure = common.parseLegPart(l{"dep"})
-    result.arrival = common.parseLegPart(l{"arr"})
-
-    result.cancelled = l{"dep"}{"dCncl"}.getBool(false) or l{"arr"}{"aCncl"}.getBool(false)
-
-  return parseLeg
-
diff --git a/src/backend/hafas/parse/line.nim b/src/backend/hafas/parse/line.nim
@@ -1,71 +0,0 @@
-import ../types
-import ../util
-import ./products
-import json
-import options
-import tables
-import httpClient
-import asyncdispatch
-
-
-var trainTypes = initTable[string, string]()
-var trainTypesShort = initTable[string, string]()
-
-proc fetchTrainTypes() {.async.} =
-  var client = newAsyncHttpClient()
-  let resp = await client.getContent("https://lib.finalrewind.org/dbdb/ice_type.json")
-  let data = parseJson(resp)
-  for key, info in pairs(data):
-    if info{"type"}.getStr != "" and info{"type"}.getStr != "EC" and info{"type"}.getStr != "IC":
-      trainTypes[key] = info{"type"}.getStr
-    if info{"short"}.getStr != "":
-      trainTypesShort[key] = info{"short"}.getStr
-
-asyncCheck fetchTrainTypes()
-
-
-proc parseLine*(common: CommonData, i: int): Option[Line] =
-  let l = common.lines[i]
-
-  # unparsable
-  if l{"cls"}.getInt == 0:
-    return options.none(Line)
-
-  let line = l.to(HafasProd)
-  var res = Line()
-
-  res.name = line.name
-  res.product = parseProduct(line.cls)
-  res.tripNum = line.prodCtx.num
-
-  if not isNone(line.prodCtx.catOut):
-    res.productName = get(line.prodCtx.catOut)
-  else:
-    res.productName = "?"
-
-  res.fullProductName = line.prodCtx.catOutL
-  res.id = slug(line.prodCtx.lineId.get(line.name))
-
-  if line.opX.isSome:
-    res.operator = some(common.operators[line.opX.get])
-
-  # DB
-
-  if res.productName == "IC" or res.productName == "ICE" or res.productName == "EC" or res.productName == "ECE":
-    if trainTypes.contains(res.tripNum) and trainTypes[res.tripNum] != res.productName:
-      res.trainType = some(trainTypes[res.tripNum])
-    if trainTypesShort.contains(res.tripNum):
-      res.trainTypeShort = some(trainTypesShort[res.tripNum])
-
-  if line.nameS.isSome and (res.product == bus or res.product == tram or res.product == ferry):
-    res.name = line.nameS.get
-
-  if line.addName.isSome:
-    # swap name and addName
-    res.additionalName = some(res.name)
-    res.name = line.addName.get
-
-  # End DB
-
-  res.mode = MODES[int(res.product)]
-  return some(res)
diff --git a/src/backend/hafas/parse/msg.nim b/src/backend/hafas/parse/msg.nim
@@ -1,12 +0,0 @@
-import ../types
-import json
-
-proc mkParseMsg*(common: CommonData): proc =
-  proc parseMsg(m: JsonNode): Remark =
-    let typeStr = m{"type"}.getStr()
-    if typeStr != "REM":
-      raise newException(CatchableError, "Unimplemented hafas msg type: " & typeStr)
-    return common.remarks[m{"remX"}.getInt()]
-
-  return parseMsg
-
diff --git a/src/backend/hafas/parse/operator.nim b/src/backend/hafas/parse/operator.nim
@@ -1,9 +0,0 @@
-import ../../../types
-import ../util
-import json
-
-proc parseOperator*(op: JsonNode): Operator =
-  return (%* {
-    "name": op{"name"},
-    "id": %* slug(op{"name"}.getStr()),
-  }).to(Operator)
diff --git a/src/backend/hafas/parse/point.nim b/src/backend/hafas/parse/point.nim
@@ -1,78 +0,0 @@
-import ../types
-import ./products
-import json
-import options
-import tables
-
-proc parsePoint*(loc: JsonNode): Point =
-  let typeStr = loc{"type"}.getStr()
-  if typeStr == "S":
-    result.stop = some(Stop(
-      id: loc{"extId"}.getStr(),
-      name: loc{"name"}.getStr(),
-      location: Location(
-        latitude: loc{"crd"}{"y"}.getInt() / 1000000,
-        longitude: loc{"crd"}{"x"}.getInt() / 1000000,
-      ),
-      products: parseProducts(loc{"pCls"}.getInt()),
-    ))
-  elif typeStr == "P":
-    result.location = some(Location(
-      id: some(loc{"extId"}.getStr()),
-      name: some(loc{"name"}.getStr()),
-      latitude: loc{"crd"}{"y"}.getInt() / 1000000,
-      longitude: loc{"crd"}{"x"}.getInt() / 1000000,
-    ))
-  elif typeStr == "A":
-    result.location = some(Location(
-      address: some(loc{"name"}.getStr()),
-      latitude: loc{"crd"}{"y"}.getInt() / 1000000,
-      longitude: loc{"crd"}{"x"}.getInt() / 1000000,
-    ))
-  else:
-    raise newException(CatchableError, "Unimplemented hafas loc type: " & typeStr)
-
-proc formatLocationIdentifier(d: Table[string, string]): string =
-  for key, val in d:
-    result &= key
-    result &= "="
-    result &= val
-    result &= "@"
-
-proc formatCoord(c: float): string =
-  return $int(c * 1000000)
-
-proc formatPoint*(point: Point): JsonNode =
-  if point.stop.isSome:
-    let stop = point.stop.get
-    return %* {
-      "type": "S",
-      "lid": formatLocationIdentifier({
-        "A": "1",
-        "L": $stop.id,
-      }.toTable),
-    }
-  elif point.location.isSome:
-    let loc = point.location.get
-    if loc.address.isSome:
-      return %* {
-        "type": "A",
-        "lid": formatLocationIdentifier({
-          "A": "2",
-          "O": loc.address.get,
-          "X": formatCoord(loc.longitude),
-          "Y": formatCoord(loc.latitude),
-        }.toTable),
-      }
-    elif loc.name.isSome and loc.id.isSome:
-      return %* {
-        "type": "P",
-        "lid": formatLocationIdentifier({
-          "A": "4",
-          "O": loc.address.get,
-          "L": loc.id.get,
-          "X": formatCoord(loc.longitude),
-          "Y": formatCoord(loc.latitude),
-        }.toTable),
-      }
-  raise newException(CatchableError, "Cannot format HAFAS location")
diff --git a/src/backend/hafas/parse/polyline.nim b/src/backend/hafas/parse/polyline.nim
@@ -1,73 +0,0 @@
-import ../types
-import json
-import options
-import math
-
-proc gpsDistance(fromLat: float, fromLon: float, toLat: float, toLon: float): float =
-  proc toRad(x: float): float = x * PI / 180
-  let dLat = toRad(toLat - fromLat)
-  let dLon = toRad(toLon - fromLon)
-  let fromLat = toRad(fromLat)
-  let toLat = toRad(toLat)
-  let a = pow(sin(dLat / 2), 2) + (pow(sin(dLon / 2), 2) * cos(fromLat) * cos(toLat))
-  let c = 2 * arctan2(sqrt(a), sqrt(1 - a))
-  return 6371 * c
-
-proc parseIntegers(str: string): seq[int] =
-  var byte = 0
-  var current = 0
-  var bits = 0
-  for c in str:
-    byte = int(c) - 63
-    current = current or (( byte and 31 ) shl bits)
-    bits += 5
-
-    if byte < 32:
-      if (current and 1) == 1:
-        current = -current
-      current = current shr 1
-
-      result.add(current)
-
-      current = 0
-      bits = 0
-
-proc mkParsePolyline*(points: seq[Point]): proc =
-  proc parsePolyline(l: JsonNode): Polyline =
-    let line = l.to(HafasPolyLine)
-  
-    result.type = "FeatureCollection"
-  
-    var lat = 0
-    var lon = 0
-  
-    let ints = parseIntegers(line.crdEncYX)
-    var i = 0
-    while i < len(ints):
-      lat += ints[i]
-      lon += ints[i+1]
-      result.features.add(Feature(
-        type: "Feature",
-        geometry: FeatureGeometry(
-          type: "Point",
-          coordinates: @[lon / 100000, lat / 100000],
-        ),
-      ))
-      i += 2
-  
-    if line.ppLocRefL.isSome:
-      for p in line.ppLocRefL.get:
-        result.features[p.ppIdx].properties = points[p.locX].stop
-
-      # sort out coordinates closer than 5m to their neighbours
-      var j = 1
-      while true:
-        if j >= len(result.features): break
-        let last = result.features[j-1].geometry.coordinates
-        let current = result.features[j].geometry.coordinates
-        if gpsDistance(last[1], last[0], current[1], current[0]) <= 0.005:
-          result.features.delete(j)
-          continue
-        j += 1
-
-  return parsePolyline
diff --git a/src/backend/hafas/parse/products.nim b/src/backend/hafas/parse/products.nim
@@ -1,37 +0,0 @@
-import ../../../types
-import bitops
-
-proc parseProduct*(cls: int): Product =
-  var tmp = cls
-  var res = 0
-  while tmp > 1:
-    tmp = tmp shr 1
-    res += 1
-
-  return Product(res)
-
-proc parseProducts*(pCls: int): Products =
-  return Products(
-    nationalExp: pCls.testBit(0),
-    national:        pCls.testBit(1),
-    regionalExp:     pCls.testBit(2),
-    regional:        pCls.testBit(3),
-    suburban:        pCls.testBit(4),
-    bus:             pCls.testBit(5),
-    ferry:           pCls.testBit(6),
-    subway:          pCls.testBit(7),
-    tram:            pCls.testBit(8),
-    taxi:            pCls.testBit(9),
-  )
-
-proc formatProducts*(p: Products): int =
-  if p.nationalExp: result.setBit(0)
-  if p.national:        result.setBit(1)
-  if p.regionalExp:     result.setBit(2)
-  if p.regional:        result.setBit(3)
-  if p.suburban:        result.setBit(4)
-  if p.bus:             result.setBit(5)
-  if p.ferry:           result.setBit(6)
-  if p.subway:          result.setBit(7)
-  if p.tram:            result.setBit(8)
-  if p.taxi:            result.setBit(9)
diff --git a/src/backend/hafas/parse/remark.nim b/src/backend/hafas/parse/remark.nim
@@ -1,32 +0,0 @@
-import ../../../types
-import json
-
-proc parseRemark*(rem: JsonNode): Remark =
-  let typeStr = rem{"type"}.getStr()
-  if typeStr == "M" or typeStr == "P":
-    return (%* {
-      "type": %* "status",
-      "summary": rem{"txtS"},
-      "code": rem{"code"},
-      "text": rem{"txtN"},
-    }).to(Remark)
-  elif typeStr == "L":
-    return (%* {
-      "type": %* "status",
-      "code": %* "alternative-trip",
-      "text": rem{"txtN"},
-      "tripId": rem{"jid"},
-    }).to(Remark)
-  elif typeStr == "A" or typeStr == "I" or typeStr == "H":
-    return (%* {
-      "type": %* "hint",
-      "code": rem{"code"},
-      "text": rem{"txtN"},
-    }).to(Remark)
-  else:
-    # TODO: parse more accurately
-    return (%* {
-      "type": %* "status",
-      "code": rem{"code"},
-      "text": rem{"txtN"},
-    }).to(Remark)
diff --git a/src/backend/hafas/parse/stopover.nim b/src/backend/hafas/parse/stopover.nim
@@ -1,33 +0,0 @@
-import ../types
-import ./date
-import options
-import json
-
-proc parseStopoverPart(common: CommonData, mode: string, h: HafasStopParams): StopoverPart =
-  
-  if (mode != "arrival"):
-    result.plannedPlatform = h.dPlatfS
-    result.prognosedPlatform = h.dPlatfR
-    result.plannedTime  = parseDate(common, h.dTimeS, h.dTZOffset)
-    result.prognosedTime = parseDate(common, h.dTimeR, h.dTZOffset)
-  else:
-    result.plannedPlatform   = h.aPlatfS
-    result.prognosedPlatform = h.aPlatfR
-    result.plannedTime       = parseDate(common, h.aTimeS, h.aTZOffset)
-    result.prognosedTime     = parseDate(common, h.aTimeR, h.aTZOffset)
-
-proc mkParseStopover*(common: CommonData): proc =
-  proc parseStopover(s: JsonNode): Stopover =
-    let typeStr = s{"type"}.getStr()
-    if typeStr != "N":
-      echo pretty(s)
-      raise newException(CatchableError, "Unimplemented hafas stopover type: " & typeStr)
-
-    let h = s.to(HafasStopParams)
-    result.stop = common.points[s{"locX"}.getInt()].stop.get
-    result.cancelled    = h.aCncl.get(false) or h.dCncl.get(false)
-    result.arrival      = common.parseStopoverPart("arrival", h)
-    result.departure    = common.parseStopoverPart("departure", h)
-
-  return parseStopover
-
diff --git a/src/backend/hafas/todo b/src/backend/hafas/todo
@@ -1,10 +0,0 @@
-- implement leg parsing with nim types
-- check and pass back hafas errors
-- isAdd (is additional stop)
-- hafas information manager (him) remarks
-- cycle information
-- price information
-- reachable
-- profiles
-- mark current alternatives
-- stations
diff --git a/src/backend/hafas/types.nim b/src/backend/hafas/types.nim
@@ -1,53 +0,0 @@
-import ../../types
-import options
-export types
-import json
-
-type
-  CommonData* = object
-    lines*:       JsonNode
-    remarks*:     seq[Remark]
-    operators*:   seq[Operator]
-    points*:      seq[Point]
-    polylines*:   seq[Polyline]
-    dateStr*:     string
-    timestamp*:   int64
-
-  HafasStopParams* = object
-    aTimeS*:      Option[string]
-    aPlatfS*:     Option[string]
-    aTZOffset*:   Option[int]
-    aCncl*:       Option[bool]
-    aTimeR*:      Option[string]
-    aPlatfR*:     Option[string]
-    dTimeS*:      Option[string]
-    dPlatfS*:     Option[string]
-    dTZOffset*:   Option[int]
-    dCncl*:       Option[bool]
-    dTimeR*:      Option[string]
-    dPlatfR*:     Option[string]
-    locX*:        Option[int]
-
-  HafasProdCtx* = object
-    name*:     string
-    num*:      string
-    catOut*:   Option[string]
-    catOutL*:  string
-    lineId*:   Option[string]
-
-  HafasProd* = object
-    name*:     string
-    cls*:      int
-    icoX*:     int
-    nameS*:    Option[string]
-    addName*:  Option[string]
-    opX*:      Option[int]
-    prodCtx*:  HafasProdCtx
-
-  HafasLocRef* = object
-    ppIdx*: int
-    locX*: int
-
-  HafasPolyline* = object
-    crdEncYX*: string
-    ppLocRefL*: Option[seq[HafasLocRef]]
diff --git a/src/backend/hafas/util.nim b/src/backend/hafas/util.nim
@@ -1,57 +0,0 @@
-import httpclient
-import asyncdispatch
-import md5
-import json
-import strutils
-import errors
-
-proc slug*(s: string): string =
-  for c in s:
-    if c.isAlphaNumeric():
-      result &= c.toLowerAscii()
-    else:
-      result &= '-'
-
-proc request*(req: JsonNode): Future[JsonNode] {.async.} =
-  let client = newAsyncHttpClient()
-
-  let body = %*{
-    "lang": "de",
-    "svcReqL": [req]
-  }
-
-  # TODO: move to profile
-  body["svcReqL"][0]["cfg"]["rtMode"] = %* "HYBRID"
-  body["client"] = %* {
-    "id": "DB",
-    "v": "16040000",
-    "type": "IPH",
-    "name": "DB Navigator"
-  }
-  body["ext"] = %* "DB.R19.04.a"
-  body["ver"] = %* "1.16"
-  body["auth"] = %* {
-    "type": "AID",
-    "aid": "n91dB8Z77MLdoR0K"
-  }
-  let salt = "bdI8UVj40K5fvxwf"
-  let bodytext = $body
-  let checksum = $toMD5(bodytext & salt)
-  let url = "https://reiseauskunft.bahn.de/bin/mgate.exe?checksum=" & checksum
-
-  client.headers = newHttpHeaders({
-    "Content-Type": "application/json",
-    "Accept": "application/json",
-    "user-agent": "my-awesome-e5f276d8fe6cprogram",
-  })
-
-  #echo pretty body
-  let req = await client.request(url, httpMethod = HttpPost, body = $body)
-  let resp = await req.body
-  let data = parseJson(resp)
-
-  let error = data{"svcResL"}{0}{"err"}.getStr()
-  if error != "OK":
-    raise parseError(error)
-
-  return data{"svcResL"}{0}
diff --git a/src/endpoints/journeys.nim b/src/endpoints/journeys.nim
@@ -1,5 +1,5 @@
 import json, tables, options, asyncdispatch
-import ../types, ../backend/hafas, ../cache
+import ../types, ../hafas/hafas, ../cache
 
 when not defined(release):
   import hotcodereloading
diff --git a/src/endpoints/moreJourneys.nim b/src/endpoints/moreJourneys.nim
@@ -1,5 +1,5 @@
 import json, tables, options, asyncdispatch, strutils
-import ../types, ../cache_types, ../backend/hafas, ../cache
+import ../types, ../cache_types, ../hafas/hafas, ../cache
 
 proc moreJourneysEndpoint*(requestData: JsonNode): Future[JsonNode] {.async.} =
   var reqId = requestData{"reqId"}.getStr()

@@ -39,4 +39,4 @@ proc moreJourneysEndpoint*(requestData: JsonNode): Future[JsonNode] {.async.} =
 
     return response
   else:
-    raise newException(errorException, "MISSING_VALUES")-
\ No newline at end of file
+    raise newException(errorException, "MISSING_VALUES")
diff --git a/src/endpoints/refreshJourney.nim b/src/endpoints/refreshJourney.nim
@@ -1,5 +1,5 @@
 import json, tables, options, asyncdispatch, strutils
-import ../types, ../cache_types, ../backend/hafas, ../cache
+import ../types, ../cache_types, ../hafas/hafas, ../cache
 
 proc refreshJourneyEndpoint*(requestData: JsonNode): Future[JsonNode] {.async.} =
   let reqId     = requestData{"reqId"}.getStr()

@@ -34,4 +34,4 @@ proc refreshJourneyEndpoint*(requestData: JsonNode): Future[JsonNode] {.async.} 
     delete(response["journeys"][journeyId]["legs"][legKey], "cycle")
     delete(response["journeys"][journeyId]["legs"][legKey], "tripId")
 
-  return response-
\ No newline at end of file
+  return response
diff --git a/src/endpoints/suggestions.nim b/src/endpoints/suggestions.nim
@@ -1,5 +1,5 @@
 import json, options, asyncdispatch
-import ../types, ../backend/hafas
+import ../types, ../hafas/hafas
 
 const ds100Json = staticRead "../../ds100reverse.json"
 
diff --git a/src/hafas b/src/hafas
@@ -0,0 +1 @@
+Subproject commit e815bbfd2ef4f49c4e3a757739a60d802bcc53f5