Initial commit

This commit is contained in:
Laura Hausmann 2020-02-26 18:32:35 +01:00
commit 918ceb8e77
Signed by: zotan
GPG Key ID: 5EC1D38FFC321311
52 changed files with 5443 additions and 0 deletions

249
.gitignore vendored Normal file
View File

@ -0,0 +1,249 @@
# Created by https://www.gitignore.io/api/rider,jetbrains,dotnetcore,jetbrains+all,jetbrains+iml
# Edit at https://www.gitignore.io/?templates=rider,jetbrains,dotnetcore,jetbrains+all,jetbrains+iml
### DotnetCore ###
# .NET Core build folders
bin
obj
# Common node modules locations
node_modules
wwwroot/node_modules
### JetBrains ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### JetBrains Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
.idea/**/sonarlint/
# SonarQube Plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator/
### JetBrains+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### JetBrains+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
### JetBrains+iml ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
### JetBrains+iml Patch ###
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
### Rider ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
# Generated files
# Sensitive or high-churn files
# Gradle
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
# Mongo Explorer plugin
# File-based project format
# IntelliJ
# mpeltonen/sbt-idea plugin
# JIRA plugin
# Cursive Clojure plugin
# Crashlytics plugin (for Android Studio and IntelliJ)
# Editor-based Rest Client
# Android studio 3.1+ serialized cache file
# End of https://www.gitignore.io/api/rider,jetbrains,dotnetcore,jetbrains+all,jetbrains+iml

View File

@ -0,0 +1,25 @@
using System;
namespace ZTravel.API.DBF {
public class DbfAjaxObject {
public bool HasDepartureTime;
public bool HasArrivalTime;
public DateTime DepartureTime;
public DateTime ArrivalTime;
public string TrainName;
public string TrainClass;
public string TrainSubtype;
public string TrainNumber;
public string Line;
public string LineType;
public string Origin;
public string Destination;
public string Route;
public bool PlatformChanged = false;
public string Platform;
public int Delay;
public string Info;
public bool Cancelled;
public bool Replacement;
}
}

View File

@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Web;
using System.Xml.Linq;
// ReSharper disable PossibleNullReferenceException
namespace ZTravel.API.DBF {
public class DbfAjaxParser {
public static List<DbfAjaxObject> GetDepartures(string station, string via = "") {
var url = $"https://dbf.finalrewind.org/{HttpUtility.UrlEncode(station)}?via={via}&show_realtime=1&ajax=1";
var result = new List<DbfAjaxObject>();
var raw = new WebClient().DownloadString(url);
var xel = XElement.Parse($"<root>{raw}</root>", LoadOptions.PreserveWhitespace);
foreach (var el in xel.Elements()) {
var connection = new DbfAjaxObject();
if (!string.IsNullOrWhiteSpace(el.Attribute("data-line")?.Value))
connection.Line = el.Attribute("data-line")?.Value.Trim();
if (!string.IsNullOrWhiteSpace(el.Attribute("data-train")?.Value))
connection.TrainName = el.Attribute("data-train")?.Value.Trim();
if (!string.IsNullOrWhiteSpace(el.Attribute("data-from")?.Value))
connection.Origin = el.Attribute("data-from")?.Value.Trim();
if (!string.IsNullOrWhiteSpace(el.Attribute("data-to")?.Value))
connection.Destination = el.Attribute("data-to")?.Value.Trim();
if (!string.IsNullOrWhiteSpace(el.Attribute("data-platform")?.Value))
connection.Platform = el.Attribute("data-platform")?.Value.Trim();
if (!string.IsNullOrWhiteSpace(el.Attribute("data-no")?.Value))
connection.TrainNumber = el.Attribute("data-no")?.Value.Trim();
if (!string.IsNullOrWhiteSpace(el.Attribute("data-linetype")?.Value))
connection.LineType = el.Attribute("data-linetype")?.Value.Trim();
if (!string.IsNullOrWhiteSpace(el.Attribute("data-arrival")?.Value)) {
connection.HasArrivalTime = true;
connection.ArrivalTime = DateTime.ParseExact(el.Attribute("data-arrival")?.Value.Trim(), "HH:mm", CultureInfo.InvariantCulture);
}
if (!string.IsNullOrWhiteSpace(el.Attribute("data-departure")?.Value)) {
connection.HasDepartureTime = true;
connection.DepartureTime = DateTime.ParseExact(el.Attribute("data-departure")?.Value.Trim(), "HH:mm", CultureInfo.InvariantCulture);
}
var route = el.Descendants("span").FirstOrDefault(div => div.Attribute("class").Value.StartsWith("route"))?.Value;
if (!string.IsNullOrWhiteSpace(route))
connection.Route = route.Trim();
var trainClass = el.Descendants("div").FirstOrDefault(div => div.Attribute("class").Value.StartsWith("line"))?.Value;
if (!string.IsNullOrWhiteSpace(trainClass))
connection.TrainClass = trainClass.Trim().Split("\n")[0].Trim();
var trainSubtype = el.Descendants("div")
.FirstOrDefault(div => div.Descendants("span").Any(span => span.Attribute("class").Value.StartsWith("trainsubtype")))
?.Descendants("span")
.FirstOrDefault(span => span.Attribute("class").Value.StartsWith("trainsubtype"))
?.Value;
if (!string.IsNullOrWhiteSpace(trainSubtype))
connection.TrainSubtype = trainSubtype.Trim();
var trainNumber = el.Descendants("div")
.FirstOrDefault(div => div.Descendants("span").Any(span => span.Attribute("class").Value.StartsWith("trainno")))
?.Descendants("span")
.FirstOrDefault(span => span.Attribute("class").Value.StartsWith("trainno"))
?.Value;
if (!string.IsNullOrWhiteSpace(trainNumber))
connection.TrainNumber = trainNumber.Trim();
var platform = el.Descendants("span").FirstOrDefault(div => div.Attribute("class").Value.StartsWith("platform"));
if (platform.Attribute("class")?.Value == "platform changed-platform") {
connection.Platform = platform.Value.Trim();
connection.PlatformChanged = true;
}
var dest = el.Descendants("span").FirstOrDefault(div => div.Attribute("class").Value.StartsWith("dest"));
if (dest.Attribute("class")?.Value == "dest cancelled")
connection.Cancelled = true;
result.Add(connection);
}
return result;
}
}
}

15
ZTravel.API/DBF/DbfApi.cs Normal file
View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Net;
using ZTravel.API.VRRF;
namespace ZTravel.API.DBF {
public class DbfApi {
public static List<Departure> GetDepartures(string api, string station, string via = "") {
var url =
$"https://{api}/{WebUtility.UrlEncode(station)}?show_realtime=1&via={WebUtility.UrlEncode(via)}&mode=json&version=3";
var raw = new WebClient().DownloadString(url);
var json = DbfResponse.FromJson(raw);
return json.Departures;
}
}
}

View File

@ -0,0 +1,83 @@
// <auto-generated />
//
// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do:
//
// using ZTravel.API.DBF;
//
// var dbfResponse = DbfResponse.FromJson(jsonString);
namespace ZTravel.API.DBF
{
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using J = Newtonsoft.Json.JsonPropertyAttribute;
using R = Newtonsoft.Json.Required;
using N = Newtonsoft.Json.NullValueHandling;
public partial class DbfResponse
{
[J("departures")] public List<Departure> Departures { get; set; }
}
public partial class Departure
{
[J("delayArrival")] public int? DelayArrival { get; set; }
[J("delayDeparture")] public int? DelayDeparture { get; set; }
[J("destination")] public string Destination { get; set; }
[J("isCancelled")] public long IsCancelled { get; set; }
[J("messages")] public Messages Messages { get; set; }
[J("platform")] public string Platform { get; set; }
[J("route")] public List<Route> Route { get; set; }
[J("scheduledArrival")] public string ScheduledArrival { get; set; }
[J("scheduledDeparture")] public string ScheduledDeparture { get; set; }
[J("scheduledPlatform")] public string ScheduledPlatform { get; set; }
[J("train")] public string Train { get; set; }
[J("trainClasses")] public List<string> TrainClasses { get; set; }
[J("trainNumber")] public string TrainNumber { get; set; }
[J("via")] public List<string> Via { get; set; }
}
public partial class Messages
{
[J("delay")] public List<Delay> Delay { get; set; }
[J("qos")] public List<Delay> Qos { get; set; }
}
public partial class Delay
{
[J("text")] public string Text { get; set; }
[J("timestamp")] public string Timestamp { get; set; }
}
public partial class Route
{
[J("name")] public string Name { get; set; }
}
public partial class DbfResponse
{
public static DbfResponse FromJson(string json) => JsonConvert.DeserializeObject<DbfResponse>(json, ZTravel.API.DBF.Converter.Settings);
}
public static partial class Serialize
{
public static string ToJson(this DbfResponse self) => JsonConvert.SerializeObject(self, ZTravel.API.DBF.Converter.Settings);
}
internal static partial class Converter
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
}
}

View File

@ -0,0 +1,37 @@
// <auto-generated />
//
// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do:
//
// using ZTravel.API.DBF;
//
// var dbfTypeMapping = DbfTypeMapping.FromJson(jsonString);
namespace ZTravel.API.DBF
{
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using J = Newtonsoft.Json.JsonPropertyAttribute;
using R = Newtonsoft.Json.Required;
using N = Newtonsoft.Json.NullValueHandling;
public partial class DbfTypeMapping
{
[J("raw")] public string Raw { get; set; }
[J("short", NullValueHandling = N.Ignore)] public string Short { get; set; }
[J("type")] public string Type { get; set; }
}
public partial class DbfTypeMapping
{
public static Dictionary<string, DbfTypeMapping> FromJson(string json) => JsonConvert.DeserializeObject<Dictionary<string, DbfTypeMapping>>(json, ZTravel.API.DBF.Converter.Settings);
}
public static partial class Serialize
{
public static string ToJson(this Dictionary<string, DbfTypeMapping> self) => JsonConvert.SerializeObject(self, ZTravel.API.DBF.Converter.Settings);
}
}

View File

@ -0,0 +1,9 @@
using dirkj.hafas.net;
namespace ZTravel.API.HAFAS {
public class HafasApi {
public void test() {
var provider = new HafasProvider(HafasEndpoints.DSB);
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Net;
using System.Web;
namespace ZTravel.API.Marudor.HAFAS.Details {
public class MarudorHafasDetailsApi {
public static MarudorHafasDetailsResponse GetDetails(string train, long epoch = 0) {
var url = $"https://marudor.de/api/hafas/v1/details/{train}";
Console.WriteLine(url);
var raw = new WebClient().DownloadString(url);
var json = MarudorHafasDetailsResponse.FromJson(raw);
return json;
}
}
}

View File

@ -0,0 +1,152 @@
// <auto-generated />
//
// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do:
//
// using ZTravel.API.Marudor.HAFAS.Details;
//
// var marudorHafasDetailsResponse = MarudorHafasDetailsResponse.FromJson(jsonString);
namespace ZTravel.API.Marudor.HAFAS.Details
{
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using J = Newtonsoft.Json.JsonPropertyAttribute;
using R = Newtonsoft.Json.Required;
using N = Newtonsoft.Json.NullValueHandling;
public partial class MarudorHafasDetailsResponse
{
[J("arrival")] public Arrival Arrival { get; set; }
[J("departure")] public Arrival Departure { get; set; }
[J("duration")] public long Duration { get; set; }
[J("plannedSequence")] public PlannedSequence PlannedSequence { get; set; }
[J("train")] public Train Train { get; set; }
[J("segmentStart")] public Segment SegmentStart { get; set; }
[J("segmentDestination")] public Segment SegmentDestination { get; set; }
[J("stops")] public List<Stop> Stops { get; set; }
[J("finalDestination")] public string FinalDestination { get; set; }
[J("jid")] public string Jid { get; set; }
[J("auslastung")] public Auslastung Auslastung { get; set; }
[J("messages")] public List<Message> Messages { get; set; }
[J("type")] public string Type { get; set; }
}
public partial class Arrival
{
[J("platform")] public string Platform { get; set; }
[J("scheduledTime")] public long ScheduledTime { get; set; }
[J("time")] public long Time { get; set; }
[J("reihung")] public bool Reihung { get; set; }
}
public partial class Auslastung
{
[J("first")] public long First { get; set; }
[J("second")] public long Second { get; set; }
}
public partial class Message
{
[J("type")] public string Type { get; set; }
[J("code")] public string Code { get; set; }
[J("prio")] public long Prio { get; set; }
[J("icoX")] public long IcoX { get; set; }
[J("txtN")] public string TxtN { get; set; }
}
public partial class PlannedSequence
{
[J("raw")] public string Raw { get; set; }
[J("short")] public string Short { get; set; }
[J("type")] public string Type { get; set; }
[J("wagons")] public Wagons Wagons { get; set; }
}
public partial class Wagons
{
[J("Apmzf")] public bool Apmzf { get; set; }
[J("Avmz")] public bool Avmz { get; set; }
[J("BRmz")] public bool BRmz { get; set; }
[J("Bpmbz")] public bool Bpmbz { get; set; }
[J("Bpmz")] public bool Bpmz { get; set; }
[J("Bpmzf")] public bool Bpmzf { get; set; }
[J("Bvmz")] public bool Bvmz { get; set; }
[J("WRmz")] public bool WRmz { get; set; }
}
public partial class Segment
{
[J("title")] public string Title { get; set; }
[J("id")] public string Id { get; set; }
}
public partial class Stop
{
[J("station")] public Station Station { get; set; }
[J("departure", NullValueHandling = N.Ignore)] public Arrival Departure { get; set; }
[J("auslastung", NullValueHandling = N.Ignore)] public Auslastung Auslastung { get; set; }
[J("arrival", NullValueHandling = N.Ignore)] public Arrival Arrival { get; set; }
[J("irisMessages", NullValueHandling = N.Ignore)] public List<IrisMessage> IrisMessages { get; set; }
}
public partial class IrisMessage
{
[J("text")] public string Text { get; set; }
[J("timestamp")] public long Timestamp { get; set; }
}
public partial class Station
{
[J("id")] public string Id { get; set; }
[J("title")] public string Title { get; set; }
[J("coordinates")] public Coordinates Coordinates { get; set; }
}
public partial class Coordinates
{
[J("lng")] public double Lng { get; set; }
[J("lat")] public double Lat { get; set; }
}
public partial class Train
{
[J("name")] public string Name { get; set; }
[J("admin")] public string Admin { get; set; }
[J("number")] public string Number { get; set; }
[J("type")] public string Type { get; set; }
[J("operator")] public Operator Operator { get; set; }
}
public partial class Operator
{
[J("name")] public string Name { get; set; }
[J("icoX")] public long IcoX { get; set; }
}
public partial class MarudorHafasDetailsResponse
{
public static MarudorHafasDetailsResponse FromJson(string json) => JsonConvert.DeserializeObject<MarudorHafasDetailsResponse>(json, ZTravel.API.Marudor.HAFAS.Details.Converter.Settings);
}
public static class Serialize
{
public static string ToJson(this MarudorHafasDetailsResponse self) => JsonConvert.SerializeObject(self, ZTravel.API.Marudor.HAFAS.Details.Converter.Settings);
}
internal static class Converter
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
}
}

View File

@ -0,0 +1,5 @@
namespace ZTravel.API.Marudor.Wagenreihung {
public class WagenreihungApi {
}
}

View File

@ -0,0 +1,156 @@
// <auto-generated />
//
// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do:
//
// using ZTravel.API.Marudor.Wagenreihung;
//
// var wagenreihungResponse = WagenreihungResponse.FromJson(jsonString);
namespace ZTravel.API.Marudor.Wagenreihung
{
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using J = Newtonsoft.Json.JsonPropertyAttribute;
using R = Newtonsoft.Json.Required;
using N = Newtonsoft.Json.NullValueHandling;
public partial class WagenreihungResponse
{
[J("fahrtrichtung")] public string Fahrtrichtung { get; set; }
[J("allFahrzeuggruppe")] public List<AllFahrzeuggruppe> AllFahrzeuggruppe { get; set; }
[J("halt")] public Halt Halt { get; set; }
[J("liniebezeichnung")] public string Liniebezeichnung { get; set; }
[J("zuggattung")] public string Zuggattung { get; set; }
[J("zugnummer")] public string Zugnummer { get; set; }
[J("serviceid")] public string Serviceid { get; set; }
[J("planstarttag")] public string Planstarttag { get; set; }
[J("fahrtid")] public string Fahrtid { get; set; }
[J("istplaninformation")] public bool Istplaninformation { get; set; }
[J("differentDestination")] public bool DifferentDestination { get; set; }
[J("differentZugnummer")] public bool DifferentZugnummer { get; set; }
[J("realFahrtrichtung")] public bool RealFahrtrichtung { get; set; }
[J("scale")] public double Scale { get; set; }
[J("startPercentage")] public long StartPercentage { get; set; }
[J("endPercentage")] public long EndPercentage { get; set; }
[J("isRealtime")] public bool IsRealtime { get; set; }
}
public partial class AllFahrzeuggruppe
{
[J("allFahrzeug")] public List<AllFahrzeug> AllFahrzeug { get; set; }
[J("fahrzeuggruppebezeichnung")] public string Fahrzeuggruppebezeichnung { get; set; }
[J("zielbetriebsstellename")] public string Zielbetriebsstellename { get; set; }
[J("startbetriebsstellename")] public string Startbetriebsstellename { get; set; }
[J("verkehrlichezugnummer")] public string Verkehrlichezugnummer { get; set; }
[J("br")] public Br Br { get; set; }
[J("goesToFrance")] public bool GoesToFrance { get; set; }
[J("tzn")] public string Tzn { get; set; }
[J("name")] public string Name { get; set; }
[J("startPercentage")] public long StartPercentage { get; set; }
[J("endPercentage")] public long EndPercentage { get; set; }
}
public partial class AllFahrzeug
{
[J("allFahrzeugausstattung")] public List<AllFahrzeugausstattung> AllFahrzeugausstattung { get; set; }
[J("kategorie")] public string Kategorie { get; set; }
[J("fahrzeugnummer")] public string Fahrzeugnummer { get; set; }
[J("orientierung")] public string Orientierung { get; set; }
[J("positioningruppe")] public string Positioningruppe { get; set; }
[J("fahrzeugsektor")] public string Fahrzeugsektor { get; set; }
[J("fahrzeugtyp")] public string Fahrzeugtyp { get; set; }
[J("wagenordnungsnummer")] public string Wagenordnungsnummer { get; set; }
[J("positionamhalt")] public Positionam Positionamhalt { get; set; }
[J("status")] public string Status { get; set; }
[J("additionalInfo")] public AdditionalInfo AdditionalInfo { get; set; }
}
public partial class AdditionalInfo
{
[J("klasse")] public long Klasse { get; set; }
[J("icons")] public Icons Icons { get; set; }
[J("comfort", NullValueHandling = N.Ignore)] public bool? Comfort { get; set; }
[J("comfortSeats", NullValueHandling = N.Ignore)] public string ComfortSeats { get; set; }
[J("disabledSeats", NullValueHandling = N.Ignore)] public string DisabledSeats { get; set; }
}
public partial class Icons
{
[J("quiet", NullValueHandling = N.Ignore)] public bool? Quiet { get; set; }
[J("family", NullValueHandling = N.Ignore)] public bool? Family { get; set; }
[J("disabled", NullValueHandling = N.Ignore)] public bool? Disabled { get; set; }
[J("dining", NullValueHandling = N.Ignore)] public bool? Dining { get; set; }
[J("info", NullValueHandling = N.Ignore)] public bool? Info { get; set; }
[J("toddler", NullValueHandling = N.Ignore)] public bool? Toddler { get; set; }
[J("wheelchair", NullValueHandling = N.Ignore)] public bool? Wheelchair { get; set; }
}
public partial class AllFahrzeugausstattung
{
[J("anzahl")] public string Anzahl { get; set; }
[J("ausstattungsart")] public string Ausstattungsart { get; set; }
[J("bezeichnung")] public string Bezeichnung { get; set; }
[J("status")] public string Status { get; set; }
}
public partial class Positionam
{
[J("endemeter")] public string Endemeter { get; set; }
[J("endeprozent")] public string Endeprozent { get; set; }
[J("startmeter")] public string Startmeter { get; set; }
[J("startprozent")] public string Startprozent { get; set; }
}
public partial class Br
{
[J("name")] public string Name { get; set; }
[J("BR")] public string BrBr { get; set; }
[J("country")] public string Country { get; set; }
[J("showBRInfo")] public bool ShowBrInfo { get; set; }
}
public partial class Halt
{
[J("abfahrtszeit")] public string Abfahrtszeit { get; set; }
[J("ankunftszeit")] public string Ankunftszeit { get; set; }
[J("bahnhofsname")] public string Bahnhofsname { get; set; }
[J("evanummer")] public string Evanummer { get; set; }
[J("gleisbezeichnung")] public string Gleisbezeichnung { get; set; }
[J("haltid")] public string Haltid { get; set; }
[J("rl100")] public string Rl100 { get; set; }
[J("allSektor")] public List<AllSektor> AllSektor { get; set; }
}
public partial class AllSektor
{
[J("positionamgleis")] public Positionam Positionamgleis { get; set; }
[J("sektorbezeichnung")] public string Sektorbezeichnung { get; set; }
}
public partial class WagenreihungResponse
{
public static WagenreihungResponse FromJson(string json) => JsonConvert.DeserializeObject<WagenreihungResponse>(json, ZTravel.API.Marudor.Wagenreihung.Converter.Settings);
}
public static class Serialize
{
public static string ToJson(this WagenreihungResponse self) => JsonConvert.SerializeObject(self, ZTravel.API.Marudor.Wagenreihung.Converter.Settings);
}
internal static class Converter
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Net;
namespace ZTravel.API.VRRF {
public class VrrfApi {
public static VrrfResponse GetDepartures(string api, string backend, string city, string station, int offset = 0, string line = "", string platform = "") {
var url =
$"https://{api}/{city}/{WebUtility.UrlEncode(station)}.json?backend={backend}&offset={offset}&line={line}&platform={platform}";
var raw = new WebClient().DownloadString(url);
var json = VrrfResponse.FromJson(raw);
return json;
}
}
}

View File

@ -0,0 +1,86 @@
// <auto-generated />
//
// To parse this JSON data, add NuGet 'Newtonsoft.Json' then do:
//
// using ZTravel.API.VRRF;
//
// var vrrfResponse = VrrfResponse.FromJson(jsonString);
namespace ZTravel.API.VRRF
{
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using J = Newtonsoft.Json.JsonPropertyAttribute;
using R = Newtonsoft.Json.Required;
using N = Newtonsoft.Json.NullValueHandling;
public partial class VrrfResponse
{
[J("error")] public object Error { get; set; }
[J("preformatted")] public List<List<string>> Preformatted { get; set; }
[J("raw")] public List<Raw> Raw { get; set; }
[J("version")] public string Version { get; set; }
}
public partial class Raw
{
[J("countdown")] public string Countdown { get; set; }
[J("date")] public string Date { get; set; }
[J("delay")] public string Delay { get; set; }
[J("destination")] public string Destination { get; set; }
[J("info")] public string Info { get; set; }
[J("is_cancelled")] public long IsCancelled { get; set; }
[J("key")] public string Key { get; set; }
[J("line")] public string Line { get; set; }
[J("lineref")] public Lineref Lineref { get; set; }
[J("mot")] public string Mot { get; set; }
[J("next_route")] public List<object> NextRoute { get; set; }
[J("platform")] public string Platform { get; set; }
[J("platform_db")] public long PlatformDb { get; set; }
[J("platform_name")] public string PlatformName { get; set; }
[J("prev_route")] public List<object> PrevRoute { get; set; }
[J("sched_date")] public string SchedDate { get; set; }
[J("sched_time")] public string SchedTime { get; set; }
[J("time")] public string Time { get; set; }
[J("type")] public string Type { get; set; }
}
public partial class Lineref
{
[J("direction")] public string Direction { get; set; }
[J("identifier")] public string Identifier { get; set; }
[J("mot")] public string Mot { get; set; }
[J("name")] public string Name { get; set; }
[J("operator")] public string Operator { get; set; }
[J("route")] public string Route { get; set; }
[J("type")] public string Type { get; set; }
[J("valid")] public string Valid { get; set; }
}
public partial class VrrfResponse
{
public static VrrfResponse FromJson(string json) => JsonConvert.DeserializeObject<VrrfResponse>(json, ZTravel.API.VRRF.Converter.Settings);
}
public static class Serialize
{
public static string ToJson(this VrrfResponse self) => JsonConvert.SerializeObject(self, ZTravel.API.VRRF.Converter.Settings);
}
internal static class Converter
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Folder Include="VRRF" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="dirkj.hafas.net" Version="1.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
</Project>

45
ZTravel.CLI/Program.cs Normal file
View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using ZTravel.API.VRRF;
using ZTravel.API.DBF;
namespace ZTravel.CLI {
internal static class Departures {
private static void Mainold(string[] args) {
if (args[0] == "vrrf") {
var deps = args.Length switch {
6 => VrrfApi.GetDepartures(args[1], args[2], args[3], args[4], platform: args[5]).Raw,
5 => VrrfApi.GetDepartures(args[1], args[2], args[3], args[4]).Raw,
_ => new List<Raw>()
};
foreach (var dep in deps) {
var rawtime = DateTime.ParseExact($"{dep.SchedDate} {dep.Time}", "dd.MM.yyyy HH:mm", CultureInfo.InvariantCulture);
var countdownRaw = rawtime - DateTime.Now;
var countdown = Math.Round(countdownRaw.TotalMinutes);
var time = countdown <= 60 ? $"{countdown,2} min" : $"{dep.Time,6}";
if (countdown <= 0)
time = " now";
if (countdown <= -1)
continue;
Console.WriteLine($"{dep.Line,-3} -> {dep.Destination.Replace($"{args[3]} ", ""),-30} {time}");
}
}
if (args[0] == "dbf") {
var deps = args.Length switch {
3 => DbfApi.GetDepartures(args[1], args[2]),
4 => DbfApi.GetDepartures(args[1], args[2], args[3]),
_ => new List<Departure>()
};
foreach (var dep in deps.Where(p => p.ScheduledDeparture != null)) {
Console.WriteLine($"{dep.Train,-10} -> {dep.Destination,-30} {dep.ScheduledDeparture}");
}
}
}
}
}

13
ZTravel.CLI/TestMain.cs Normal file
View File

@ -0,0 +1,13 @@
using System;
using ZTravel.API.DBF;
namespace ZTravel.CLI {
public class TestMain {
private static void Main(string[] args) {
var deps = DbfAjaxParser.GetDepartures("München Hbf");
foreach (var dep in deps) {
Console.WriteLine(dep.Line);
}
}
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ZTravel.API\ZTravel.API.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,53 @@
@page
@using System.Globalization
@using ZTravel.API.VRRF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Departures - ZTravel</title>
<meta http-equiv="refresh" content="30">
<link rel="stylesheet" href="~/css/site.css"/>
</head>
<body>
<div class="navbar-fixed">
<nav style="color: #ffffff; background-color: #00838f;">
<div class="nav-wrapper container">
<span class="brand-logo">Salzburg Maria-Cebotari-Straße (2)</span>
</div>
</nav>
</div>
<div class="app applight">
<ul>
@foreach (var dep in VrrfApi.GetDepartures("vrrf.finalrewind.org", "efa.SVV", "salzburg", "cebotari", platform: "2").Raw) {
var rawtime = DateTime.ParseExact($"{dep.SchedDate} {dep.Time}", "dd.MM.yyyy HH:mm", CultureInfo.InvariantCulture);
var countdownRaw = rawtime - DateTime.Now;
var countdown = Math.Round(countdownRaw.TotalMinutes);
var time = countdown <= 60 ? $"{countdown} min" : dep.Time;
if (countdown <= 0) {
time = "now";
}
if (countdown <= -1) {
continue;
}
<li>
@if (int.Parse(dep.Line) < 14) {
<div class="line obus obus-line-@dep.Line">@dep.Line</div>
}
else {
<div class="line bus">@dep.Line</div>
}
<div class="dest">@dep.Destination</div>
<span class="route">@dep.Lineref.Route</span>
<span class="countdown">
<span class="platform">@time</span>
</span>
<span class="time">@dep.Time</span>
</li>
}
</ul>
</div>
</body>
</html>

View File

@ -0,0 +1,64 @@
@page
@using System.Globalization
@using ZTravel.API.VRRF
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Departures - ZTravel</title>
<meta http-equiv="refresh" content="30">
<link rel="stylesheet" href="~/css/space.css"/>
<style>
body {
zoom: 130% !important;
}
</style>
</head>
<body>
<!--
<div class="navbar-fixed">
<nav style="color: #ffffff; background-color: #00838f;">
<div class="nav-wrapper container">
<span class="brand-logo">Salzburg Justizgebäude (+5)</span>
</div>
</nav>
</div>
-->
<div class="app appdark">
<ul>
@{
var counter = 0;
}
@foreach (var dep in VrrfApi.GetDepartures("vrrf.finalrewind.org", "efa.SVV", "salzburg", "maria-cebotari-straße", platform: "2").Raw) {
if (int.Parse(dep.Line) >= 20) {
continue;
}
counter++;
var rawtime = DateTime.ParseExact($"{dep.Date} {dep.Time}", "dd.MM.yyyy HH:mm", CultureInfo.InvariantCulture);
var countdownRaw = rawtime - DateTime.Now;
var countdown = Math.Round(countdownRaw.TotalMinutes);
var time = countdown <= 60 ? $"{countdown} min" : dep.Time;
if (countdown <= 0) {
time = "now";
}
if (countdown <= 5) {
continue;
}
<li>
@if (int.Parse(dep.Line) < 14) {
<div class="line obus">@dep.Line</div>
}
else {
<div class="line bus">@dep.Line</div>
}
<div class="dest">@dep.Destination</div>
<span class="countdown">
<span class="platform">@time</span>
</span>
</li>
}
</ul>
</div>
</body>
</html>

View File

@ -0,0 +1,26 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
namespace ZTravel.Pages {
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel {
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
private readonly ILogger<ErrorModel> _logger;
public ErrorModel(ILogger<ErrorModel> logger) {
_logger = logger;
}
public void OnGet() {
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
}

View File

@ -0,0 +1,40 @@
@page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Departures - ZTravel</title>
</head>
<body>
<style>
body {
background-color: #FFFFFF;
}
.wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
height: 98vh;
}
.item {
display: block;
width: 100%;
height: 100%;
border: none;
}
</style>
<div class="wrapper">
<!--<iframe class="item" src="https://vrrf.finalrewind.org/Salzburg/Maria-Cebotari-Strasse.html?frontend=html&backend=efa.SVV&no_lines=10"></iframe>
<iframe class="item" src="https://vrrf.finalrewind.org/Salzburg%20Parsch.html?frontend=html&backend=hafas.DB&no_lines=10"></iframe> -->
<iframe scrolling="no" class="item" src="/Cebotari"></iframe>
<iframe scrolling="no" class="item" src="/ParschBhf"></iframe>
<iframe scrolling="no" class="item" src="/SbgMuc"></iframe>
<iframe scrolling="no" class="item" src="/SbgHbf"></iframe>
</div>
</body>
</html>

View File

@ -0,0 +1,40 @@
@page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Departures - ZTravel</title>
</head>
<body>
<style>
body {
background-color: #FFFFFF;
}
.wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
height: 98vh;
}
.item {
display: block;
width: 100%;
height: 100%;
border: none;
}
</style>
<div class="wrapper">
<!--<iframe class="item" src="https://vrrf.finalrewind.org/Salzburg/Maria-Cebotari-Strasse.html?frontend=html&backend=efa.SVV&no_lines=10"></iframe>
<iframe class="item" src="https://vrrf.finalrewind.org/Salzburg%20Parsch.html?frontend=html&backend=hafas.DB&no_lines=10"></iframe> -->
<iframe scrolling="no" class="item" src="/Cebotari"></iframe>
<iframe scrolling="no" class="item" src="/ParschBhf"></iframe>
<iframe scrolling="no" class="item" src="/SbgMuc"></iframe>
<iframe scrolling="no" class="item" src="/SbgHbf"></iframe>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
@page
@{
Layout = "_LayoutFern";
ViewData["title"] = "Karlsruhe Hbf (+30, excl → Weingarten)";
ViewData["station"] = "Karlsruhe Hbf";
ViewData["stationId"] = "8079041";
ViewData["via"] = "";
ViewData["excludingVia"] = "Weingarten(Baden)";
ViewData["offset"] = 30;
ViewData["andShowDestination"] = "";
ViewData["fernverkehrOnly"] = false;
ViewData["onlyDepartures"] = true;
}

View File

@ -0,0 +1,33 @@
@page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Departures - ZTravel</title>
</head>
<body>
<style>
body {
background-color: #FFFFFF;
}
.item {
display: block;
width: 100%;
height: 300px;
border: none;
}
</style>
<div class="wrapper">
<!--<iframe class="item" src="https://vrrf.finalrewind.org/Salzburg/Maria-Cebotari-Strasse.html?frontend=html&backend=efa.SVV&no_lines=10"></iframe>
<iframe class="item" src="https://vrrf.finalrewind.org/Salzburg%20Parsch.html?frontend=html&backend=hafas.DB&no_lines=10"></iframe> -->
<iframe scrolling="no" class="item" src="/Cebotari"></iframe>
<iframe scrolling="no" class="item" src="/ParschBhf"></iframe>
<iframe scrolling="no" class="item" src="/SbgHbf"></iframe>
<iframe scrolling="no" class="item" src="/MucHbf"></iframe>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
@page
@{
Layout = "_LayoutFern";
ViewData["title"] = "München Hbf";
ViewData["station"] = "München Hbf";
ViewData["stationId"] = "8000261";
ViewData["via"] = "";
ViewData["excludingVia"] = "";
ViewData["offset"] = 0;
ViewData["andShowDestination"] = "Salzburg Hbf";
ViewData["fernverkehrOnly"] = true;
ViewData["onlyDepartures"] = false;
}

View File

@ -0,0 +1,13 @@
@page
@{
Layout = "_LayoutFern";
ViewData["title"] = "Salzburg Parsch";
ViewData["station"] = "Salzburg Parsch";
ViewData["stationId"] = "8101481";
ViewData["via"] = "";
ViewData["excludingVia"] = "";
ViewData["offset"] = 0;
ViewData["andShowDestination"] = "";
ViewData["fernverkehrOnly"] = false;
ViewData["onlyDepartures"] = false;
}

View File

@ -0,0 +1,13 @@
@page
@{
Layout = "_LayoutFern";
ViewData["title"] = "Salzburg Hbf (+20, excl → Parsch)";
ViewData["station"] = "Salzburg Hbf";
ViewData["stationId"] = "8100002";
ViewData["via"] = "";
ViewData["excludingVia"] = "Salzburg Parsch";
ViewData["offset"] = 20;
ViewData["andShowDestination"] = "";
ViewData["fernverkehrOnly"] = false;
ViewData["onlyDepartures"] = true;
}

View File

@ -0,0 +1,13 @@
@page
@{
Layout = "_LayoutFern";
ViewData["title"] = "Salzburg Hbf (+20, → München Hbf)";
ViewData["station"] = "Salzburg Hbf";
ViewData["stationId"] = "8100002";
ViewData["via"] = "München Hbf";
ViewData["excludingVia"] = "";
ViewData["offset"] = 20;
ViewData["andShowDestination"] = "";
ViewData["fernverkehrOnly"] = false;
ViewData["onlyDepartures"] = true;
}

View File

@ -0,0 +1 @@
@RenderBody()

View File

@ -0,0 +1,159 @@
@{
// ReSharper disable ConditionIsAlwaysTrueOrFalse
var title = (string) ViewData["title"];
var station = (string) ViewData["station"];
var stationId = (string) ViewData["stationId"];
var via = (string) ViewData["via"];
var andShowDestination = (string) ViewData["andShowDestination"];
var fernverkehrOnly = (bool) ViewData["fernverkehrOnly"];
var onlyDepartures = (bool) ViewData["onlyDepartures"];
var excludingVia = (string) ViewData["excludingVia"];
var offset = (int) ViewData["offset"];
IgnoreBody();
}
@using System.Globalization
@using System.Linq
@using ZTravel.API.DBF
@using ZTravel.Web
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Departures - ZTravel</title>
<meta http-equiv="refresh" content="30">
<link rel="stylesheet" href="~/css/fernv.css"/>
</head>
<body>
<div class="navbar-fixed">
<nav style="color: #ffffff; background-color: #00838f;">
<div class="nav-wrapper container">
<span class="brand-logo">@title</span>
</div>
</nav>
</div>
<div class="app applight">
<ul>
@foreach (var dep in DbfApi.GetDepartures("dbf.finalrewind.org", station, via).Where(dep => !fernverkehrOnly || dep.TrainClasses.Contains("F") || dep.Destination == andShowDestination).Where(dep => !onlyDepartures || !string.IsNullOrWhiteSpace(dep.ScheduledDeparture))) {
if (dep.Route.Select(r => r.Name).Contains(excludingVia)) {
continue;
}
var typeclass = "bahn";
if (dep.TrainClasses.Any()) {
typeclass = dep.TrainClasses.First() switch {
"S" => "sbahn",
"F" => "fern",
_ => "bahn"};
}
var trainclass = dep.Train.Split(" ")[0];
DateTime rawtime;
var delayed = false;
var undelayed = false;
var cancelled = dep.IsCancelled == 1;
var stringtime = "";
var stringdelay = "";
if (!string.IsNullOrWhiteSpace(dep.ScheduledDeparture)) {
rawtime = DateTime.ParseExact(dep.ScheduledDeparture, "HH:mm", CultureInfo.InvariantCulture);
if (dep.DelayDeparture != null) {
rawtime += new TimeSpan(0, (int) dep.DelayDeparture, 0);
if (dep.DelayDeparture > 0) {
delayed = true;
stringdelay = $"(+{dep.DelayDeparture})";
}
else if (dep.DelayDeparture < 0) {
undelayed = true;
stringdelay = $"({dep.DelayDeparture})";
}
}
stringtime = $"{rawtime:HH:mm}";
}
else {
rawtime = DateTime.ParseExact(dep.ScheduledArrival, "HH:mm", CultureInfo.InvariantCulture);
if (dep.DelayArrival != null) {
rawtime += new TimeSpan(0, (int) dep.DelayArrival, 0);
if (dep.DelayArrival > 0) {
delayed = true;
stringdelay = $"(+{dep.DelayArrival})";
}
else if (dep.DelayArrival < 0) {
undelayed = true;
stringdelay = $"({dep.DelayArrival})";
}
}
stringtime = $"(arr) {rawtime:HH:mm}";
dep.Destination += $" (from {dep.Route.First().Name})";
}
if ((rawtime - DateTime.Now).TotalMinutes < 0) {
rawtime += new TimeSpan(1, 0, 0, 0);
}
if ((rawtime - DateTime.Now).TotalMinutes < offset) {
continue;
}
<li class="@(cancelled ? " cancelled" : "")" onclick="openInNewTab('https://marudor.de/details/@trainclass @dep.TrainNumber?station=@stationId')">
<div class="line @typeclass">
@if (trainclass == "S") {
@dep.Train