use submodule for hafas-client
This commit is contained in:
parent
2ff5e8936b
commit
16580bc856
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "src/hafas"]
|
||||||
|
path = src/hafas
|
||||||
|
url = https://cgit.ctu.cx/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
|
|
|
@ -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)
|
|
|
@ -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]
|
|
|
@ -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)
|
|
|
@ -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,
|
|
||||||
)
|
|
|
@ -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"
|
|
|
@ -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())
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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()
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -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")
|
|
|
@ -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
|
|
|
@ -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)
|
|
|
@ -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)
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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]]
|
|
|
@ -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}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import json, tables, options, asyncdispatch
|
import json, tables, options, asyncdispatch
|
||||||
import ../types, ../backend/hafas, ../cache
|
import ../types, ../hafas/hafas, ../cache
|
||||||
|
|
||||||
when not defined(release):
|
when not defined(release):
|
||||||
import hotcodereloading
|
import hotcodereloading
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import json, tables, options, asyncdispatch, strutils
|
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.} =
|
proc moreJourneysEndpoint*(requestData: JsonNode): Future[JsonNode] {.async.} =
|
||||||
var reqId = requestData{"reqId"}.getStr()
|
var reqId = requestData{"reqId"}.getStr()
|
||||||
|
@ -39,4 +39,4 @@ proc moreJourneysEndpoint*(requestData: JsonNode): Future[JsonNode] {.async.} =
|
||||||
|
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
raise newException(errorException, "MISSING_VALUES")
|
raise newException(errorException, "MISSING_VALUES")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import json, tables, options, asyncdispatch, strutils
|
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.} =
|
proc refreshJourneyEndpoint*(requestData: JsonNode): Future[JsonNode] {.async.} =
|
||||||
let reqId = requestData{"reqId"}.getStr()
|
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], "cycle")
|
||||||
delete(response["journeys"][journeyId]["legs"][legKey], "tripId")
|
delete(response["journeys"][journeyId]["legs"][legKey], "tripId")
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import json, options, asyncdispatch
|
import json, options, asyncdispatch
|
||||||
import ../types, ../backend/hafas
|
import ../types, ../hafas/hafas
|
||||||
|
|
||||||
const ds100Json = staticRead "../../ds100reverse.json"
|
const ds100Json = staticRead "../../ds100reverse.json"
|
||||||
|
|
||||||
|
|
1
src/hafas
Submodule
1
src/hafas
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit e815bbfd2ef4f49c4e3a757739a60d802bcc53f5
|
Loading…
Reference in a new issue