From 17a3ea880402338420699e03bcb24181e4ff3924 Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Thu, 2 May 2024 20:27:40 +0200 Subject: Initial commit Based on dc4ba6a --- src/querykv1/.envrc | 2 + src/querykv1/.gitignore | 1 + src/querykv1/Makefile | 28 +++ src/querykv1/cliopts.cpp | 456 ++++++++++++++++++++++++++++++++++++++++++ src/querykv1/cliopts.hpp | 35 ++++ src/querykv1/daterange.cpp | 91 +++++++++ src/querykv1/daterange.hpp | 118 +++++++++++ src/querykv1/grammar.abnf | 44 ++++ src/querykv1/grammar.ebnf | 47 +++++ src/querykv1/grammar.ebnf.bak | 23 +++ src/querykv1/joparoute.cpp | 102 ++++++++++ src/querykv1/joparoute.hpp | 13 ++ src/querykv1/journeyinfo.cpp | 64 ++++++ src/querykv1/journeyinfo.hpp | 13 ++ src/querykv1/journeyroute.cpp | 96 +++++++++ src/querykv1/journeyroute.hpp | 13 ++ src/querykv1/journeys.cpp | 95 +++++++++ src/querykv1/journeys.hpp | 13 ++ src/querykv1/main.cpp | 198 ++++++++++++++++++ src/querykv1/schedule.cpp | 63 ++++++ src/querykv1/schedule.hpp | 13 ++ 21 files changed, 1528 insertions(+) create mode 100644 src/querykv1/.envrc create mode 100644 src/querykv1/.gitignore create mode 100644 src/querykv1/Makefile create mode 100644 src/querykv1/cliopts.cpp create mode 100644 src/querykv1/cliopts.hpp create mode 100644 src/querykv1/daterange.cpp create mode 100644 src/querykv1/daterange.hpp create mode 100644 src/querykv1/grammar.abnf create mode 100644 src/querykv1/grammar.ebnf create mode 100644 src/querykv1/grammar.ebnf.bak create mode 100644 src/querykv1/joparoute.cpp create mode 100644 src/querykv1/joparoute.hpp create mode 100644 src/querykv1/journeyinfo.cpp create mode 100644 src/querykv1/journeyinfo.hpp create mode 100644 src/querykv1/journeyroute.cpp create mode 100644 src/querykv1/journeyroute.hpp create mode 100644 src/querykv1/journeys.cpp create mode 100644 src/querykv1/journeys.hpp create mode 100644 src/querykv1/main.cpp create mode 100644 src/querykv1/schedule.cpp create mode 100644 src/querykv1/schedule.hpp (limited to 'src/querykv1') diff --git a/src/querykv1/.envrc b/src/querykv1/.envrc new file mode 100644 index 0000000..694e74f --- /dev/null +++ b/src/querykv1/.envrc @@ -0,0 +1,2 @@ +source_env ../../ +export DEVMODE=1 diff --git a/src/querykv1/.gitignore b/src/querykv1/.gitignore new file mode 100644 index 0000000..5761abc --- /dev/null +++ b/src/querykv1/.gitignore @@ -0,0 +1 @@ +*.o diff --git a/src/querykv1/Makefile b/src/querykv1/Makefile new file mode 100644 index 0000000..a8791f5 --- /dev/null +++ b/src/querykv1/Makefile @@ -0,0 +1,28 @@ +# Taken from: +# Open Source Security Foundation (OpenSSF), “Compiler Options Hardening Guide +# for C and C++,” OpenSSF Best Practices Working Group. Accessed: Dec. 01, +# 2023. [Online]. Available: +# https://best.openssf.org/Compiler-Hardening-Guides/Compiler-Options-Hardening-Guide-for-C-and-C++.html +CXXFLAGS=-std=c++2b -g -fno-omit-frame-pointer $(if $(DEVMODE),-Werror,)\ + -O2 -Wall -Wformat=2 -Wconversion -Wtrampolines -Wimplicit-fallthrough \ + -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 \ + -D_GLIBCXX_ASSERTIONS \ + -fstrict-flex-arrays=3 \ + -fstack-clash-protection -fstack-protector-strong +LDFLAGS=-ltmi8 -Wl,-z,defs \ + -Wl,-z,nodlopen -Wl,-z,noexecstack \ + -Wl,-z,relro -Wl,-z,now + +HDRS=cliopts.hpp daterange.hpp joparoute.hpp journeyinfo.hpp journeyroute.hpp journeys.hpp schedule.hpp +SRCS=main.cpp cliopts.cpp daterange.cpp joparoute.cpp journeyinfo.cpp journeyroute.cpp journeys.cpp schedule.cpp +OBJS=$(patsubst %.cpp,%.o,$(SRCS)) + +%.o: %.cpp $(HDRS) + $(CXX) -c -o $@ $< $(CXXFLAGS) + +querykv1: $(OBJS) + $(CXX) -fPIE -pie -o $@ $^ $(CXXFLAGS) $(LDFLAGS) + +.PHONY: clean +clean: + rm querykv1 diff --git a/src/querykv1/cliopts.cpp b/src/querykv1/cliopts.cpp new file mode 100644 index 0000000..bef7a98 --- /dev/null +++ b/src/querykv1/cliopts.cpp @@ -0,0 +1,456 @@ +// vim:set sw=2 ts=2 sts et: + +#include +#include +#include +#include + +#include + +#include "cliopts.hpp" + +using namespace std::string_view_literals; + +const char *opt_set = ""; +const char *opt_unset = nullptr; + +const char help[] = R"(Usage: %1$s [OPTIONS] + +Global Options: + --kv1 Path to file containing all KV1 data, '-' for stdin + -h, --help Print this help + +Commands: + joparoute Generate CSV for journey pattern route + journeyinfo Print some information on a journey + journeyroute Generate CSV for journey route + journeys List journeys of a specific line going from stop A to B + schedule Generate schedule +)"; + +const char joparoute_help[] = R"(Usage: %1$s joparoute --line --jopa [OPTIONS] + +Options: + --line Line planning number as in schedule + --jopa Journey pattern code as in KV1 data + -o Path of file to write to, '-' for stdout + +Global Options: + --kv1 Path to file containing all KV1 data, '-' for stdin + -h, --help Print this help +)"; + +const char journeyroute_help[] = R"(Usage: %1$s journeyroute --line [OPTIONS] + +Options: + --line Line planning number as in KV1 data + --journey Journey number as in KV1 data + -o Path of file to write to, '-' for stdout + +Global Options: + --kv1 Path to file containing all KV1 data, '-' for stdin + -h, --help Print this help +)"; + +const char journeys_help[] = R"(Usage: %1$s journeys --line --begin --end [OPTIONS] + +For the --begin and --end arguments, use the following format: + --begin/--end stop: + --begin/--end star: + +Options: + --begin User stop code/area of stop the journey should begin at + --end User stop code/area of stop the journey should end at + --line Line planning number to filter on + -o Path of file to write to, '-' for stdout + +Global Options: + --kv1 Path to file containing all KV1 data, '-' for stdin + -h, --help Print this help +)"; + +const char journeyinfo_help[] = R"(Usage: %1$s journeyinfo --line --journey [OPTIONS] + +Options: + --line Line planning number to filter on + --journey Journey number as in schedule + +Global Options: + --kv1 Path to file containing all KV1 data, '-' for stdin + -h, --help Print this help +)"; + +const char schedule_help[] = R"(Usage: %1$s schedule --line [OPTIONS] + +Options: + --line Line planning number to generate schedule for + -o Path of file to write to, '-' for stdout + +Global Options: + --kv1 Path to file containing all KV1 data, '-' for stdin + -h, --help Print this help +)"; + +void journeyRouteValidateOptions(const char *progname, Options *options) { +#define X(name, argument, long_, short_) \ + if (#name != "kv1_file_path"sv && #name != "line_planning_number"sv \ + && #name != "journey_number"sv && #name != "help"sv && #name != "output_file_path"sv) \ + if (options->name) { \ + if (long_) { \ + if (short_) fprintf(stderr, "%s: unexpected flag --%s (-%c) for journeyroute subcommand\n\n", progname, static_cast(long_), short_); \ + else fprintf(stderr, "%s: unexpected flag --%s for journeyroute subcommand\n\n", progname, static_cast(long_)); \ + } else if (short_) fprintf(stderr, "%s: unexpected flag -%c for journeyroute subcommand\n\n", progname, short_); \ + fprintf(stderr, journeyroute_help, progname); \ + exit(1); \ + } + LONG_OPTIONS + SHORT_OPTIONS +#undef X + + if (options->positional.size() > 0) { + fprintf(stderr, "%s: unexpected positional argument(s) for journeyroute subcommand\n\n", progname); + for (auto pos : options->positional) fprintf(stderr, "opt: %s\n", pos); + fprintf(stderr, journeyroute_help, progname); + exit(1); + } + + if (!options->kv1_file_path) + options->kv1_file_path = "-"; + if (!options->output_file_path) + options->output_file_path = "-"; + if (options->kv1_file_path == ""sv) { + fprintf(stderr, "%s: KV1 file path cannot be empty\n\n", progname); + fprintf(stderr, journeyroute_help, progname); + exit(1); + } + if (options->output_file_path == ""sv) { + fprintf(stderr, "%s: output file path cannot be empty\n\n", progname); + fprintf(stderr, journeyroute_help, progname); + exit(1); + } + if (!options->journey_number || options->journey_number == ""sv) { + fprintf(stderr, "%s: journey number must be provided\n\n", progname); + fprintf(stderr, journeyroute_help, progname); + exit(1); + } + if (!options->line_planning_number || options->line_planning_number == ""sv) { + fprintf(stderr, "%s: line planning number must be provided\n\n", progname); + fprintf(stderr, journeyroute_help, progname); + exit(1); + } +} + +void scheduleValidateOptions(const char *progname, Options *options) { +#define X(name, argument, long_, short_) \ + if (#name != "kv1_file_path"sv && #name != "help"sv \ + && #name != "line_planning_number"sv && #name != "output_file_path"sv) \ + if (options->name) { \ + if (long_) { \ + if (short_) fprintf(stderr, "%s: unexpected flag --%s (-%c) for schedule subcommand\n\n", progname, static_cast(long_), short_); \ + else fprintf(stderr, "%s: unexpected flag --%s for schedule subcommand\n\n", progname, static_cast(long_)); \ + } else if (short_) fprintf(stderr, "%s: unexpected flag -%c for schedule subcommand\n\n", progname, short_); \ + fprintf(stderr, schedule_help, progname); \ + exit(1); \ + } + LONG_OPTIONS + SHORT_OPTIONS +#undef X + + if (options->positional.size() > 0) { + fprintf(stderr, "%s: unexpected positional argument(s) for schedule subcommand\n\n", progname); + for (auto pos : options->positional) fprintf(stderr, "opt: %s\n", pos); + fprintf(stderr, schedule_help, progname); + exit(1); + } + + if (!options->kv1_file_path) + options->kv1_file_path = "-"; + if (!options->output_file_path) + options->output_file_path = "-"; + if (options->kv1_file_path == ""sv) { + fprintf(stderr, "%s: KV1 file path cannot be empty\n\n", progname); + fprintf(stderr, schedule_help, progname); + exit(1); + } + if (options->output_file_path == ""sv) { + fprintf(stderr, "%s: output file path cannot be empty\n\n", progname); + fprintf(stderr, schedule_help, progname); + exit(1); + } + if (!options->line_planning_number || options->line_planning_number == ""sv) { + fprintf(stderr, "%s: line planning number must be provided\n\n", progname); + fprintf(stderr, schedule_help, progname); + exit(1); + } +} + +void journeysValidateOptions(const char *progname, Options *options) { +#define X(name, argument, long_, short_) \ + if (#name != "kv1_file_path"sv && #name != "help"sv \ + && #name != "line_planning_number"sv && #name != "output_file_path"sv \ + && #name != "begin_stop_code"sv && #name != "end_stop_code"sv) \ + if (options->name) { \ + if (long_) { \ + if (short_) fprintf(stderr, "%s: unexpected flag --%s (-%c) for journeys subcommand\n\n", progname, static_cast(long_), short_); \ + else fprintf(stderr, "%s: unexpected flag --%s for journeys subcommand\n\n", progname, static_cast(long_)); \ + } else if (short_) fprintf(stderr, "%s: unexpected flag -%c for journeys subcommand\n\n", progname, short_); \ + fprintf(stderr, journeys_help, progname); \ + exit(1); \ + } + LONG_OPTIONS + SHORT_OPTIONS +#undef X + + if (options->positional.size() > 0) { + fprintf(stderr, "%s: unexpected positional argument(s) for journeys subcommand\n\n", progname); + for (auto pos : options->positional) fprintf(stderr, "opt: %s\n", pos); + fprintf(stderr, journeys_help, progname); + exit(1); + } + + if (!options->kv1_file_path) + options->kv1_file_path = "-"; + if (!options->output_file_path) + options->output_file_path = "-"; + if (options->kv1_file_path == ""sv) { + fprintf(stderr, "%s: KV1 file path cannot be empty\n\n", progname); + fprintf(stderr, journeys_help, progname); + exit(1); + } + if (options->output_file_path == ""sv) { + fprintf(stderr, "%s: output file path cannot be empty\n\n", progname); + fprintf(stderr, journeys_help, progname); + exit(1); + } + if (!options->line_planning_number || options->line_planning_number == ""sv) { + fprintf(stderr, "%s: line planning number must be provided\n\n", progname); + fprintf(stderr, journeys_help, progname); + exit(1); + } + if (!options->begin_stop_code || options->begin_stop_code == ""sv) { + fprintf(stderr, "%s: start user stop code must be provided\n\n", progname); + fprintf(stderr, journeys_help, progname); + exit(1); + } + if (!options->end_stop_code || options->end_stop_code == ""sv) { + fprintf(stderr, "%s: end user stop code must be provided\n\n", progname); + fprintf(stderr, journeys_help, progname); + exit(1); + } + if (!std::string_view(options->begin_stop_code).starts_with("star:") + && !std::string_view(options->begin_stop_code).starts_with("stop:")) { + fprintf(stderr, "%s: begin user stop code must be prefixed with star:/stop:\n\n", progname); + fprintf(stderr, journeys_help, progname); + exit(1); + } + if (!std::string_view(options->end_stop_code).starts_with("star:") + && !std::string_view(options->end_stop_code).starts_with("stop:")) { + fprintf(stderr, "%s: end user stop code must be prefixed with star:/stop:\n\n", progname); + fprintf(stderr, journeys_help, progname); + exit(1); + } +} + +void journeyInfoValidateOptions(const char *progname, Options *options) { +#define X(name, argument, long_, short_) \ + if (#name != "kv1_file_path"sv && #name != "line_planning_number"sv \ + && #name != "journey_number"sv && #name != "help"sv) \ + if (options->name) { \ + if (long_) { \ + if (short_) fprintf(stderr, "%s: unexpected flag --%s (-%c) for journeyinfo subcommand\n\n", progname, static_cast(long_), short_); \ + else fprintf(stderr, "%s: unexpected flag --%s for journeyinfo subcommand\n\n", progname, static_cast(long_)); \ + } else if (short_) fprintf(stderr, "%s: unexpected flag -%c for journeyinfo subcommand\n\n", progname, short_); \ + fprintf(stderr, journeyinfo_help, progname); \ + exit(1); \ + } + LONG_OPTIONS + SHORT_OPTIONS +#undef X + + if (options->positional.size() > 0) { + fprintf(stderr, "%s: unexpected positional argument(s) for journeyinfo subcommand\n\n", progname); + for (auto pos : options->positional) fprintf(stderr, "opt: %s\n", pos); + fprintf(stderr, journeyinfo_help, progname); + exit(1); + } + + if (!options->kv1_file_path) + options->kv1_file_path = "-"; + if (options->kv1_file_path == ""sv) { + fprintf(stderr, "%s: KV1 file path cannot be empty\n\n", progname); + fprintf(stderr, journeyinfo_help, progname); + exit(1); + } + if (!options->journey_number || options->journey_number == ""sv) { + fprintf(stderr, "%s: journey number must be provided\n\n", progname); + fprintf(stderr, journeyinfo_help, progname); + exit(1); + } + if (!options->line_planning_number || options->line_planning_number == ""sv) { + fprintf(stderr, "%s: line planning number must be provided\n\n", progname); + fprintf(stderr, journeyinfo_help, progname); + exit(1); + } +} + +void jopaRouteValidateOptions(const char *progname, Options *options) { +#define X(name, argument, long_, short_) \ + if (#name != "kv1_file_path"sv && #name != "line_planning_number"sv \ + && #name != "journey_pattern_code"sv && #name != "help"sv && #name != "output_file_path"sv) \ + if (options->name) { \ + if (long_) { \ + if (short_) fprintf(stderr, "%s: unexpected flag --%s (-%c) for joparoute subcommand\n\n", progname, static_cast(long_), short_); \ + else fprintf(stderr, "%s: unexpected flag --%s for joparoute subcommand\n\n", progname, static_cast(long_)); \ + } else if (short_) fprintf(stderr, "%s: unexpected flag -%c for joparoute subcommand\n\n", progname, short_); \ + fprintf(stderr, joparoute_help, progname); \ + exit(1); \ + } + LONG_OPTIONS + SHORT_OPTIONS +#undef X + + if (options->positional.size() > 0) { + fprintf(stderr, "%s: unexpected positional argument(s) for joparoute subcommand\n\n", progname); + for (auto pos : options->positional) fprintf(stderr, "opt: %s\n", pos); + fprintf(stderr, joparoute_help, progname); + exit(1); + } + + if (!options->kv1_file_path) + options->kv1_file_path = "-"; + if (!options->output_file_path) + options->output_file_path = "-"; + if (options->kv1_file_path == ""sv) { + fprintf(stderr, "%s: KV1 file path cannot be empty\n\n", progname); + fprintf(stderr, joparoute_help, progname); + exit(1); + } + if (options->output_file_path == ""sv) { + fprintf(stderr, "%s: output file path cannot be empty\n\n", progname); + fprintf(stderr, joparoute_help, progname); + exit(1); + } + if (!options->journey_pattern_code || options->journey_pattern_code == ""sv) { + fprintf(stderr, "%s: journey pattern code must be provided\n\n", progname); + fprintf(stderr, joparoute_help, progname); + exit(1); + } + if (!options->line_planning_number || options->line_planning_number == ""sv) { + fprintf(stderr, "%s: line planning number must be provided\n\n", progname); + fprintf(stderr, joparoute_help, progname); + exit(1); + } +} + +struct ShortFlag { + int has_arg; + int c; +}; + +template +const std::string mkargarr = + (std::string() + + ... + + (flags.c == 0 + ? "" + : std::string((const char[]){ flags.c, '\0' }) + + (flags.has_arg == required_argument + ? ":" + : flags.has_arg == optional_argument + ? "::" + : ""))); + +#define X(name, has_arg, long_, short_) ShortFlag(has_arg, short_), +const std::string argarr = mkargarr; +#undef X + +Options parseOptions(int argc, char *argv[]) { + const char *progname = argv[0]; + + // Struct with options for augmentkv6. + Options options; + + static option long_options[] = { +#define X(name, argument, long_, short_) { long_, argument, nullptr, short_ }, + LONG_OPTIONS +#undef X + { 0 }, + }; + + int c; + int option_index = 0; + bool error = false; + while ((c = getopt_long(argc, argv, argarr.c_str(), long_options, &option_index)) != -1) { + // If a long option was used, c corresponds with val. We have val = 0 for + // options which have no short alternative, so checking for c = 0 gives us + // whether a long option with no short alternative was used. + // Below, we check for c = 'h', which corresponds with the long option + // '--help', for which val = 'h'. + if (c == 0) { + const char *name = long_options[option_index].name; +#define X(opt_name, opt_has_arg, opt_long, opt_short) \ + if (name == opt_long ## sv) { options.opt_name = optarg; continue; } + LONG_OPTIONS +#undef X + error = true; + } +#define X(opt_name, opt_has_arg, opt_long, opt_short) \ + if (c == opt_short) { options.opt_name = optarg ? optarg : opt_set; continue; } + LONG_OPTIONS + SHORT_OPTIONS +#undef X + error = true; + } + + if (optind < argc) + options.subcommand = argv[optind++]; + while (optind < argc) + options.positional.push_back(argv[optind++]); + + if (options.subcommand + && options.subcommand != "schedule"sv + && options.subcommand != "joparoute"sv + && options.subcommand != "journeyinfo"sv + && options.subcommand != "journeyroute"sv + && options.subcommand != "journeys"sv) { + fprintf(stderr, "%s: unknown subcommand '%s'\n\n", progname, options.subcommand); + fprintf(stderr, help, progname); + exit(1); + } + if (options.subcommand && error) { + fputc('\n', stderr); + if (options.subcommand == "joparoute"sv) fprintf(stderr, joparoute_help, progname); + if (options.subcommand == "journeyinfo"sv) fprintf(stderr, journeyinfo_help, progname); + if (options.subcommand == "journeyroute"sv) fprintf(stderr, journeyroute_help, progname); + if (options.subcommand == "journeys"sv) fprintf(stderr, journeys_help, progname); + if (options.subcommand == "schedule"sv) fprintf(stderr, schedule_help, progname); + exit(1); + } + if (error || !options.subcommand) { + if (!options.subcommand) fprintf(stderr, "%s: no subcommand provided\n", progname); + fputc('\n', stderr); + fprintf(stderr, help, progname); + exit(1); + } + if (options.help) { + if (options.subcommand == "joparoute"sv) fprintf(stderr, joparoute_help, progname); + if (options.subcommand == "journeyinfo"sv) fprintf(stderr, journeyinfo_help, progname); + if (options.subcommand == "journeyroute"sv) fprintf(stderr, journeyroute_help, progname); + if (options.subcommand == "journeys"sv) fprintf(stderr, journeys_help, progname); + if (options.subcommand == "schedule"sv) fprintf(stderr, schedule_help, progname); + exit(0); + } + + if (options.subcommand == "joparoute"sv) + jopaRouteValidateOptions(progname, &options); + if (options.subcommand == "journeyinfo"sv) + journeyInfoValidateOptions(progname, &options); + if (options.subcommand == "journeyroute"sv) + journeyRouteValidateOptions(progname, &options); + if (options.subcommand == "journeys"sv) + journeysValidateOptions(progname, &options); + if (options.subcommand == "schedule"sv) + scheduleValidateOptions(progname, &options); + + return options; +} diff --git a/src/querykv1/cliopts.hpp b/src/querykv1/cliopts.hpp new file mode 100644 index 0000000..df8630e --- /dev/null +++ b/src/querykv1/cliopts.hpp @@ -0,0 +1,35 @@ +// vim:set sw=2 ts=2 sts et: + +#ifndef OEUF_QUERYKV1_CLIOPTS_HPP +#define OEUF_QUERYKV1_CLIOPTS_HPP + +#include + +#define LONG_OPTIONS \ +/* name req/opt/no arg long short */ + X(kv1_file_path, required_argument, "kv1", 0 ) \ + X(line_planning_number, required_argument, "line", 0 ) \ + X(journey_number, required_argument, "journey", 0 ) \ + X(journey_pattern_code, required_argument, "jopa", 0 ) \ + X(begin_stop_code, required_argument, "begin", 0 ) \ + X(end_stop_code, required_argument, "end", 0 ) \ + X(help, no_argument, "help", 'h') + +#define SHORT_OPTIONS \ + X(output_file_path, required_argument, nullptr, 'o') + +struct Options { + const char *subcommand = nullptr; + std::vector positional; +#define X(name, argument, long_, short_) const char *name = nullptr; + LONG_OPTIONS + SHORT_OPTIONS +#undef X +}; + +extern const char *opt_set; +extern const char *opt_unset; + +Options parseOptions(int argc, char *argv[]); + +#endif // OEUF_QUERYKV1_CLIOPTS_HPP diff --git a/src/querykv1/daterange.cpp b/src/querykv1/daterange.cpp new file mode 100644 index 0000000..5ce42bf --- /dev/null +++ b/src/querykv1/daterange.cpp @@ -0,0 +1,91 @@ +// vim:set sw=2 ts=2 sts et: + +#include "daterange.hpp" + +static std::chrono::year_month_day nextDay(std::chrono::year_month_day ymd) { + return std::chrono::sys_days(ymd) + std::chrono::days(1); +} + +// DateRange expresses the date range [from, thru]. +DateRange::Iterator &DateRange::Iterator::operator++() { + ymd_ = nextDay(ymd_); + return *this; +} + +std::chrono::year_month_day DateRange::Iterator::operator*() const { + return ymd_; +} + +std::chrono::year_month_day DateRange::Iterator::ymd() const { + return ymd_; +} + +DateRange::Iterator::Iterator(std::chrono::year_month_day ymd) : ymd_(ymd) {} + +DateRange::DateRange(std::chrono::year_month_day from, std::chrono::year_month_day thru) + : from_(from), thru_(thru) +{} + +DateRange::Iterator DateRange::begin() const { + return DateRange::Iterator(from_); +} + +DateRange::Iterator DateRange::end() const { + return DateRange::Iterator(nextDay(thru_)); +} + +bool DateRange::valid() const { + return from_ <= thru_; +} + +std::chrono::year_month_day DateRange::from() const { + return from_; +} + +std::chrono::year_month_day DateRange::thru() const { + return thru_; +} + +bool operator==(const DateRange::Iterator a, const DateRange::Iterator b) { + return *a == *b; +} + +DateRangeSeq::DateRangeSeq(std::initializer_list ranges) + : DateRangeSeq(ranges.begin(), ranges.end()) +{} + +DateRangeSeq DateRangeSeq::clampFrom(std::chrono::year_month_day from) const { + std::vector new_ranges; + new_ranges.reserve(ranges_.size()); + for (const DateRange range : ranges_) { + if (range.from() < from) { + if (range.thru() < from) + continue; + new_ranges.emplace_back(from, range.thru()); + } + new_ranges.push_back(range); + } + return DateRangeSeq(new_ranges.begin(), new_ranges.end()); +} + +DateRangeSeq DateRangeSeq::clampThru(std::chrono::year_month_day thru) const { + std::vector new_ranges; + new_ranges.reserve(ranges_.size()); + for (const DateRange range : ranges_) { + if (range.thru() > thru) { + if (range.from() > thru) + continue; + new_ranges.emplace_back(range.from(), thru); + } + new_ranges.push_back(range); + } + return DateRangeSeq(new_ranges.begin(), new_ranges.end()); +} + +std::vector::const_iterator DateRangeSeq::begin() const { + return ranges_.begin(); +} + +std::vector::const_iterator DateRangeSeq::end() const { + return ranges_.end(); +} diff --git a/src/querykv1/daterange.hpp b/src/querykv1/daterange.hpp new file mode 100644 index 0000000..e34c39c --- /dev/null +++ b/src/querykv1/daterange.hpp @@ -0,0 +1,118 @@ +// vim:set sw=2 ts=2 sts et: + +#ifndef OEUF_QUERYKV1_DATERANGE_HPP +#define OEUF_QUERYKV1_DATERANGE_HPP + +#include +#include +#include +#include +#include +#include + +// DateRange expresses the date range [from, thru]. +class DateRange { + public: + class Iterator { + friend class DateRange; + + public: + Iterator &operator++(); + + std::chrono::year_month_day operator*() const; + std::chrono::year_month_day ymd() const; + + private: + explicit Iterator(std::chrono::year_month_day ymd); + + std::chrono::year_month_day ymd_; + }; + + explicit DateRange(std::chrono::year_month_day from, std::chrono::year_month_day thru); + + Iterator begin() const; + Iterator end() const; + bool valid() const; + std::chrono::year_month_day from() const; + std::chrono::year_month_day thru() const; + + private: + std::chrono::year_month_day from_; + std::chrono::year_month_day thru_; +}; + +bool operator==(const DateRange::Iterator a, const DateRange::Iterator b); + +template +concept DerefsTo = requires(Tp p) { + { *p } -> std::convertible_to; +}; + +class DateRangeSeq { + // The way LE and GE are ordered makes a difference for how the sorting + // (insertion based on lower_bound) works. Do not carelessly reorder this. + enum LeGe { + GE, // >= + LE, // <= + }; + + std::vector ranges_; + + public: + template + requires DerefsTo + explicit DateRangeSeq(InputIt begin, InputIt end) { + // We convert every inclusive date range [x, y] into (x, >=) and (y, <=) + // and put these into a list, using binary search to make sure that these + // stay ordered. We then reduce this list, removing tautological + // predicates, giving us a final list of ranges that do not overlap. + + std::vector> preds; + + size_t n = 0; + for (auto it = begin; it != end; it++) { + auto &range = *it; + if (!range.valid()) continue; + + auto a = std::make_pair(range.from(), GE); + auto b = std::make_pair(range.thru(), LE); + preds.insert(std::lower_bound(preds.begin(), preds.end(), a), a); + preds.insert(std::lower_bound(preds.begin(), preds.end(), b), b); + + n++; + } + + if (preds.empty()) + return; + + assert(preds.size() >= 2); + assert(preds.front().second == GE); + assert(preds.back().second == LE); + + std::chrono::year_month_day begin_ymd = preds[0].first; + for (size_t i = 1; i < preds.size(); i++) { + if (preds[i].second == LE && (i + 1 == preds.size() || preds[i + 1].second == GE)) { + std::chrono::year_month_day end_ymd = preds[i].first; + if (!ranges_.empty() && ranges_.back().thru() == begin_ymd) + ranges_.back() = DateRange(ranges_.back().from(), end_ymd); + else + ranges_.push_back(DateRange(begin_ymd, end_ymd)); + if (i + 1 != preds.size()) { + begin_ymd = preds[i + 1].first; + i++; + } + } + } + } + + explicit DateRangeSeq(std::initializer_list ranges); + + DateRangeSeq clampFrom(std::chrono::year_month_day from) const; + DateRangeSeq clampThru(std::chrono::year_month_day thru) const; + + public: + std::vector::const_iterator begin() const; + std::vector::const_iterator end() const; +}; + +#endif // OEUF_QUERYKV1_DATERANGE_HPP diff --git a/src/querykv1/grammar.abnf b/src/querykv1/grammar.abnf new file mode 100644 index 0000000..1c93760 --- /dev/null +++ b/src/querykv1/grammar.abnf @@ -0,0 +1,44 @@ +; This grammar does *not* allow fields to contain LF, unless the entire content +; of the field is quoted. The file is simply rejected otherwise. +; I took the liberty to take some inspiration from the somewhat similar IETF RFC 4180. + +document = [header NEWLINE] (comment / record / empty-line) *(NEWLINE (comment / record / empty-line)) [NEWLINE] / header + +header = OPENBRACK *NOTCRLF +comment = SEMICOLON *NOTCRLF + +empty-line = *WHITESPACE + +record = field *(PIPE field) +field = *WHITESPACE field-data *WHITESPACE +field-data = escaped / unescaped + +; Unescaped fields are also allowed to contain double quotes, +; they are just not interpreted in any special way. +escaped = DQUOTE *(TEXTDATA / WHITESPACE / NEWLINE / PIPE / 2DQUOTE) DQUOTE +unescaped = [TEXTDATA *(*WHITESPACE (TEXTDATA / DQUOTE))] + +HTAB = %x09 ; +LF = %x0A ; +VTAB = %x0B ; +FF = %x0C ;
+CR = %x0D ; +SPACE = %x20 ; +DQUOTE = %x22 ; " +SEMICOLON = %x3B ; ; +OPENBRACK = %x5B ; [ +PIPE = %x7C ; | + +; All codepoints, except CR, LF, SPACE, FF, HTAB, VTAB, PIPE, DQUOTE +; Semicolon is included, as comments are only defined as 'lines starting with a semicolon'. +; So it should be fine if a semicolon is part of a field, the rest of the line would not +; be interpreted as a comment in that case. +TEXTDATA = %x00-08 / %x0E-1F / %x21 / %x23-5A / %x5C-7B / %x7D-10FFFF + +; Not including LF here even though TMI8/KV1 does not officially consider it +; a newline, as newlines are defined as 'CR optionally followed by LF' +WHITESPACE = SPACE / FF / HTAB / VTAB + +; All codepoints excluding CR and LF +NOTCRLF = %x00-09 / %x0B-0C / %x0E-10FFFF +NEWLINE = CR [LF] diff --git a/src/querykv1/grammar.ebnf b/src/querykv1/grammar.ebnf new file mode 100644 index 0000000..94f8cde --- /dev/null +++ b/src/querykv1/grammar.ebnf @@ -0,0 +1,47 @@ +/* This grammar does allow fields to contain stray LFs, not after any specific + * CR. I took the liberty to take some inspiration from the somewhat similar + * IETF RFC 4180. + */ +document ::= (header NEWLINE)? (comment | record | empty-line) (NEWLINE (comment | record | empty-line))* NEWLINE? | header + +header ::= OPENBRACK NOTCR* +comment ::= SEMICOLON NOTCR* + +empty-line ::= WHITESPACE* + +record ::= field (PIPE field)* +field ::= WHITESPACE* field-data WHITESPACE* +field-data ::= DQUOTE escaped DQUOTE | unescaped + +/* Unescaped fields are also allowed to contain double quotes, they are just + * not interpreted in any special way. + */ +escaped ::= (TEXTDATA | WHITESPACE | NEWLINE | PIPE | DQUOTE DQUOTE)* +unescaped ::= (TEXTDATA (WHITESPACE* (TEXTDATA | DQUOTE))*)? + +HTAB ::= #x09 /* */ +LF ::= #x0A /* */ +VTAB ::= #x0B /* */ +FF ::= #x0C /* */ +CR ::= #x0D /* */ +SPACE ::= #x20 /* */ +DQUOTE ::= #x22 /* " */ +SEMICOLON ::= #x3B /* ; */ +OPENBRACK ::= #x5B /* [ */ +PIPE ::= #x7C /* | */ + +/* All codepoints, except CR, LF, SPACE, FF, HTAB, VTAB, PIPE, DQUOTE. + * Semicolon is included, as comments are only defined as 'lines starting with + * a semicolon'. So it should be fine if a semicolon is part of a field, the + * rest of the line would not be interpreted as a comment in that case. + */ +TEXTDATA ::= [#x00-#x08#x0E-#x1F#x21#x23-#x5A#x5C-#x7B#x7D-#x10FFFF] + +/* Including LF here as TMI8/KV1 does not consider it a newline, + * as newlines are defined as 'CR optionally followed by LF' + */ +WHITESPACE ::= SPACE | LF | FF | HTAB | VTAB + +/* All codepoints excluding CR and LF */ +NOTCR ::= [#x00-#x0C#x0E-#x10FFFF] +NEWLINE ::= CR LF? diff --git a/src/querykv1/grammar.ebnf.bak b/src/querykv1/grammar.ebnf.bak new file mode 100644 index 0000000..b5acbf5 --- /dev/null +++ b/src/querykv1/grammar.ebnf.bak @@ -0,0 +1,23 @@ +document ::= (header NEWLINE)? (comment | record | empty-line) (NEWLINE (comment | record | empty-line))* NEWLINE? | header +header ::= OPENBRACK NOTCRLF* +comment ::= SEMICOLON NOTCRLF* +empty-line ::= WHITESPACE* +record ::= field (PIPE field)* +field ::= WHITESPACE* field-data WHITESPACE* +field-data ::= escaped | unescaped +escaped ::= DQUOTE (TEXTDATA | WHITESPACE | NEWLINE | PIPE | DQUOTE DQUOTE)* DQUOTE +unescaped ::= (TEXTDATA (WHITESPACE* (TEXTDATA | DQUOTE))*)? +HTAB ::= #x09 +LF ::= #x0A +VTAB ::= #x0B +FF ::= #x0C +CR ::= #x0D +SPACE ::= #x20 +DQUOTE ::= #x22 +SEMICOLON ::= #x3B +OPENBRACK ::= #x5B +PIPE ::= #x7C +WHITESPACE ::= SPACE | FF | HTAB | VTAB +NOTCRLF ::= [#x00-#x09#x0B-#x0C#x0E-#x10FFFF] +TEXTDATA ::= [#x00-#x08#x0E-#x1F#x21#x23-#x5A#x5C-#x7B#x7D-#x10FFFF] +NEWLINE ::= CR LF? diff --git a/src/querykv1/joparoute.cpp b/src/querykv1/joparoute.cpp new file mode 100644 index 0000000..94ed359 --- /dev/null +++ b/src/querykv1/joparoute.cpp @@ -0,0 +1,102 @@ +// vim:set sw=2 ts=2 sts et: + +#include +#include +#include + +#include "joparoute.hpp" + +using namespace std::string_view_literals; + +void jopaRoute(const Options &options, Kv1Records &records, Kv1Index &index) { + FILE *out = stdout; + if (options.output_file_path != "-"sv) + out = fopen(options.output_file_path, "wb"); + if (!out) { + fprintf(stderr, "Open %s: %s\n", options.output_file_path, strerrordesc_np(errno)); + exit(EXIT_FAILURE); + } + + const std::string data_owner_code = "CXX"; + Kv1JourneyPattern::Key jopa_key( + // Of course it is bad to hardcode this, but we really have no time to make + // everything nice and dynamic. We're only working with CXX data anyway, + // and provide no support for the 'Schedules and Passing Times' KV1 + // variant. + data_owner_code, + options.line_planning_number, + options.journey_pattern_code); + + const Kv1JourneyPattern *jopa = index.journey_patterns[jopa_key]; + if (!jopa) { + std::cerr << "Journey pattern not found" << std::endl; + return; + } + const Kv1Line *line = jopa->p_line; + + struct Point { + bool is_stop = false; + const Kv1JourneyPatternTimingLink *jopatili = nullptr; + const Kv1Link *link = nullptr; + const Kv1Point *point = nullptr; + double distance_since_start_of_link = 0; + double distance_since_start_of_journey = 0; + }; + std::vector points; + + for (size_t i = 0; i < records.journey_pattern_timing_links.size(); i++) { + const Kv1JourneyPatternTimingLink *jopatili = &records.journey_pattern_timing_links[i]; + if (jopatili->key.line_planning_number == jopa->key.line_planning_number + && jopatili->key.journey_pattern_code == jopa->key.journey_pattern_code) { + const Kv1Link::Key link_key(data_owner_code, jopatili->user_stop_code_begin, + jopatili->user_stop_code_end, line->transport_type); + const Kv1Link *link = index.links[link_key]; + const Kv1UserStopPoint::Key link_begin_key(data_owner_code, jopatili->user_stop_code_begin); + const Kv1UserStopPoint::Key link_end_key(data_owner_code, jopatili->user_stop_code_end); + const Kv1UserStopPoint *link_begin = index.user_stop_points[link_begin_key]; + const Kv1UserStopPoint *link_end = index.user_stop_points[link_end_key]; + + points.emplace_back(true, jopatili, link, link_begin->p_point, 0); + + for (size_t j = 0; j < records.point_on_links.size(); j++) { + Kv1PointOnLink *pool = &records.point_on_links[j]; + if (pool->key.user_stop_code_begin == jopatili->user_stop_code_begin + && pool->key.user_stop_code_end == jopatili->user_stop_code_end + && pool->key.transport_type == jopatili->p_line->transport_type) { + points.emplace_back(false, jopatili, link, pool->p_point, pool->distance_since_start_of_link); + } + } + + points.emplace_back(true, jopatili, link, link_end->p_point, link->distance); + } + } + + std::sort(points.begin(), points.end(), [](Point &a, Point &b) { + if (a.jopatili->key.timing_link_order != b.jopatili->key.timing_link_order) + return a.jopatili->key.timing_link_order < b.jopatili->key.timing_link_order; + return a.distance_since_start_of_link < b.distance_since_start_of_link; + }); + + double distance_since_start_of_journey = 0; + for (size_t i = 0; i < points.size(); i++) { + Point *p = &points[i]; + if (i > 0) { + Point *prev = &points[i - 1]; + if (p->link != prev->link) { + distance_since_start_of_journey += prev->link->distance; + } + } + p->distance_since_start_of_journey = distance_since_start_of_journey + p->distance_since_start_of_link; + } + + fputs("is_stop,link_usrstop_begin,link_usrstop_end,point_code,rd_x,rd_y,distance_since_start_of_link,distance_since_start_of_journey\n", out); + for (const auto &point : points) { + fprintf(out, "%s,%s,%s,%s,%f,%f,%f,%f\n", + point.is_stop ? "true" : "false", + point.jopatili->user_stop_code_begin.c_str(), point.jopatili->user_stop_code_end.c_str(), + point.point->key.point_code.c_str(), point.point->location_x_ew, point.point->location_y_ns, + point.distance_since_start_of_link, point.distance_since_start_of_journey); + } + + if (options.output_file_path != "-"sv) fclose(out); +} diff --git a/src/querykv1/joparoute.hpp b/src/querykv1/joparoute.hpp new file mode 100644 index 0000000..ade94e8 --- /dev/null +++ b/src/querykv1/joparoute.hpp @@ -0,0 +1,13 @@ +// vim:set sw=2 ts=2 sts et: + +#ifndef OEUF_QUERYKV1_JOPAROUTE_HPP +#define OEUF_QUERYKV1_JOPAROUTE_HPP + +#include +#include + +#include "cliopts.hpp" + +void jopaRoute(const Options &options, Kv1Records &records, Kv1Index &index); + +#endif // OEUF_QUERYKV1_JOPAROUTE_HPP diff --git a/src/querykv1/journeyinfo.cpp b/src/querykv1/journeyinfo.cpp new file mode 100644 index 0000000..bd29490 --- /dev/null +++ b/src/querykv1/journeyinfo.cpp @@ -0,0 +1,64 @@ +// vim:set sw=2 ts=2 sts et: + +#include + +#include "journeyinfo.hpp" + +void journeyInfo(const Options &options, Kv1Records &records, Kv1Index &index) { + std::cout << "Info for journey " << options.line_planning_number + << "/" << options.journey_number << std::endl; + + std::unordered_map usrstops; + for (size_t i = 0; i < records.user_stop_points.size(); i++) { + const Kv1UserStopPoint *usrstop = &records.user_stop_points[i]; + usrstops[usrstop->key.user_stop_code] = usrstop; + } + + for (const auto &pujo : records.public_journeys) { + if (pujo.key.line_planning_number != options.line_planning_number + || std::to_string(pujo.key.journey_number) != options.journey_number) + continue; + + std::vector timing_links; + for (size_t i = 0; i < records.journey_pattern_timing_links.size(); i++) { + const Kv1JourneyPatternTimingLink *jopatili = &records.journey_pattern_timing_links[i]; + if (jopatili->key.line_planning_number != options.line_planning_number + || jopatili->key.journey_pattern_code != pujo.journey_pattern_code) + continue; + timing_links.push_back(jopatili); + } + + std::sort(timing_links.begin(), timing_links.end(), [](auto a, auto b) -> bool { + return a->key.timing_link_order < b->key.timing_link_order; + }); + auto begin_stop = timing_links.front()->user_stop_code_begin; + auto end_stop = timing_links.back()->user_stop_code_end; + + const auto *begin = usrstops[begin_stop]; + const auto *end = usrstops[end_stop]; + + std::cout << " Journey pattern: " << pujo.key.line_planning_number + << "/" << pujo.journey_pattern_code << std::endl + << " Begin stop: " << begin_stop + << "; name: " << std::quoted(begin->name) + << "; town: " << std::quoted(begin->town) << std::endl + << " End stop: " << end_stop + << "; name: " << std::quoted(end->name) + << "; town: " << std::quoted(end->town) << std::endl; + + const auto *begin_star = begin->p_user_stop_area; + const auto *end_star = end->p_user_stop_area; + if (begin_star) + std::cout << " Begin stop area: " << begin_star->key.user_stop_area_code + << "; name: " << std::quoted(begin_star->name) + << ", town: " << std::quoted(begin_star->town) + << std::endl; + if (end_star) + std::cout << " End stop area: " << end_star->key.user_stop_area_code + << "; name: " << std::quoted(end_star->name) + << ", town: " << std::quoted(end_star->town) + << std::endl; + + break; + } +} diff --git a/src/querykv1/journeyinfo.hpp b/src/querykv1/journeyinfo.hpp new file mode 100644 index 0000000..2a2118d --- /dev/null +++ b/src/querykv1/journeyinfo.hpp @@ -0,0 +1,13 @@ +// vim:set sw=2 ts=2 sts et: + +#ifndef OEUF_QUERYKV1_JOURNEYINFO_HPP +#define OEUF_QUERYKV1_JOURNEYINFO_HPP + +#include +#include + +#include "cliopts.hpp" + +void journeyInfo(const Options &options, Kv1Records &records, Kv1Index &index); + +#endif // OEUF_QUERYKV1_JOURNEYINFO_HPP diff --git a/src/querykv1/journeyroute.cpp b/src/querykv1/journeyroute.cpp new file mode 100644 index 0000000..013ea1c --- /dev/null +++ b/src/querykv1/journeyroute.cpp @@ -0,0 +1,96 @@ +// vim:set sw=2 ts=2 sts et: + +#include +#include + +#include "journeyroute.hpp" + +using namespace std::string_view_literals; + +void journeyRoute(const Options &options, Kv1Records &records, Kv1Index &index) { + FILE *out = stdout; + if (options.output_file_path != "-"sv) + out = fopen(options.output_file_path, "wb"); + if (!out) { + fprintf(stderr, "Open %s: %s\n", options.output_file_path, strerrordesc_np(errno)); + exit(EXIT_FAILURE); + } + + for (auto &pujo : records.public_journeys) { + if (pujo.key.line_planning_number == options.line_planning_number && std::to_string(pujo.key.journey_number) == options.journey_number) { + fprintf(stderr, "Got PUJO %s/%s:\n", options.line_planning_number, options.journey_number); + fprintf(stderr, " Day type: %s\n", pujo.key.day_type.c_str()); + auto &pegr = *pujo.p_period_group; + fprintf(stderr, " PEGR Code: %s\n", pegr.key.period_group_code.c_str()); + fprintf(stderr, " PEGR Description: %s\n", pegr.description.c_str()); + fprintf(stderr, " SPECDAY Code: %s\n", pujo.key.specific_day_code.c_str()); + auto &timdemgrp = *pujo.p_time_demand_group; + + for (auto &pegrval : records.period_group_validities) { + if (pegrval.key.period_group_code == pegr.key.period_group_code) { + fprintf(stderr, "Got PEGRVAL for PEGR %s\n", pegr.key.period_group_code.c_str()); + std::cerr << " Valid from: " << pegrval.key.valid_from << std::endl; + std::cerr << " Valid thru: " << pegrval.valid_thru << std::endl; + } + } + + struct Point { + Kv1JourneyPatternTimingLink *jopatili = nullptr; + Kv1TimeDemandGroupRunTime *timdemrnt = nullptr; + double distance_since_start_of_link = 0; + double rd_x = 0; + double rd_y = 0; + double total_time_s = 0; + }; + std::vector points; + + for (size_t i = 0; i < records.time_demand_group_run_times.size(); i++) { + Kv1TimeDemandGroupRunTime *timdemrnt = &records.time_demand_group_run_times[i]; + if (timdemrnt->key.line_planning_number == timdemgrp.key.line_planning_number + && timdemrnt->key.journey_pattern_code == timdemgrp.key.journey_pattern_code + && timdemrnt->key.time_demand_group_code == timdemgrp.key.time_demand_group_code) { + Kv1JourneyPatternTimingLink *jopatili = timdemrnt->p_journey_pattern_timing_link; + for (auto &pool : records.point_on_links) { + if (pool.key.user_stop_code_begin == timdemrnt->user_stop_code_begin + && pool.key.user_stop_code_end == timdemrnt->user_stop_code_end + && pool.key.transport_type == jopatili->p_line->transport_type) { + points.emplace_back( + jopatili, + timdemrnt, + pool.distance_since_start_of_link, + pool.p_point->location_x_ew, + pool.p_point->location_y_ns + ); + } + } + } + } + + std::sort(points.begin(), points.end(), [](Point &a, Point &b) { + if (a.jopatili->key.timing_link_order != b.jopatili->key.timing_link_order) + return a.jopatili->key.timing_link_order < b.jopatili->key.timing_link_order; + return a.distance_since_start_of_link < b.distance_since_start_of_link; + }); + + double total_time_s = 0; + for (size_t i = 0; i < points.size(); i++) { + Point *p = &points[i]; + p->total_time_s = total_time_s; + if (i > 0) { + Point *prev = &points[i - 1]; + if (p->timdemrnt != prev->timdemrnt) { + total_time_s += prev->timdemrnt->total_drive_time_s; + prev->total_time_s = total_time_s; + } + } + } + + fputs("rd_x,rd_y,total_time_s,is_timing_stop\n", out); + for (const auto &point : points) { + fprintf(out, "%f,%f,%f,%d\n", point.rd_x, point.rd_y, point.total_time_s, point.jopatili->is_timing_stop); + } + } + } + + if (options.output_file_path != "-"sv) fclose(out); +} diff --git a/src/querykv1/journeyroute.hpp b/src/querykv1/journeyroute.hpp new file mode 100644 index 0000000..ccd996c --- /dev/null +++ b/src/querykv1/journeyroute.hpp @@ -0,0 +1,13 @@ +// vim:set sw=2 ts=2 sts et: + +#ifndef OEUF_QUERYKV1_JOURNEYROUTE_HPP +#define OEUF_QUERYKV1_JOURNEYROUTE_HPP + +#include +#include + +#include "cliopts.hpp" + +void journeyRoute(const Options &options, Kv1Records &records, Kv1Index &index); + +#endif // OEUF_QUERYKV1_JOURNEYROUTE_HPP diff --git a/src/querykv1/journeys.cpp b/src/querykv1/journeys.cpp new file mode 100644 index 0000000..96566b2 --- /dev/null +++ b/src/querykv1/journeys.cpp @@ -0,0 +1,95 @@ +// vim:set sw=2 ts=2 sts et: + +#include +#include +#include +#include + +#include "journeys.hpp" + +using namespace std::string_view_literals; + +void journeys(const Options &options, Kv1Records &records, Kv1Index &index) { + const std::string_view want_begin_stop_code(options.begin_stop_code); + const std::string_view want_end_stop_code(options.end_stop_code); + + FILE *out = stdout; + if (options.output_file_path != "-"sv) + out = fopen(options.output_file_path, "wb"); + if (!out) { + fprintf(stderr, "Open %s: %s\n", options.output_file_path, strerrordesc_np(errno)); + exit(EXIT_FAILURE); + } + + std::cerr << "Generating journeys for " << options.line_planning_number << ", going from stop " + << options.begin_stop_code << " to " << options.end_stop_code << std::endl; + + std::unordered_map usrstops; + for (size_t i = 0; i < records.user_stop_points.size(); i++) { + const Kv1UserStopPoint *usrstop = &records.user_stop_points[i]; + usrstops[usrstop->key.user_stop_code] = usrstop; + } + + std::unordered_set journey_pattern_codes; + for (const auto &jopa : records.journey_patterns) { + if (jopa.key.line_planning_number != options.line_planning_number) + continue; + journey_pattern_codes.insert(jopa.key.journey_pattern_code); + } + + std::unordered_map> jopatilis; + for (size_t i = 0; i < records.journey_pattern_timing_links.size(); i++) { + const Kv1JourneyPatternTimingLink *jopatili = &records.journey_pattern_timing_links[i]; + if (jopatili->key.line_planning_number != options.line_planning_number + || !journey_pattern_codes.contains(jopatili->key.journey_pattern_code)) + continue; + jopatilis[jopatili->key.journey_pattern_code].push_back(jopatili); + } + + std::unordered_set valid_jopas; + for (auto &[journey_pattern_code, timing_links] : jopatilis) { + std::sort(timing_links.begin(), timing_links.end(), [](auto a, auto b) -> bool { + return a->key.timing_link_order < b->key.timing_link_order; + }); + auto begin_stop = timing_links.front()->user_stop_code_begin; + auto end_stop = timing_links.back()->user_stop_code_end; + + const auto *begin = usrstops[begin_stop]; + const auto *end = usrstops[end_stop]; + + bool begin_stop_ok = false; + if (want_begin_stop_code.starts_with("stop:")) + begin_stop_ok = want_begin_stop_code.substr(5) == begin_stop; + else if (want_begin_stop_code.starts_with("star:")) + begin_stop_ok = want_begin_stop_code.substr(5) == begin->user_stop_area_code; + + bool end_stop_ok = false; + if (want_end_stop_code.starts_with("stop:")) + end_stop_ok = want_end_stop_code.substr(5) == end_stop; + else if (want_end_stop_code.starts_with("star:")) + end_stop_ok = want_end_stop_code.substr(5) == end->user_stop_area_code; + + if (begin_stop_ok && end_stop_ok) { + valid_jopas.insert(journey_pattern_code); + } + } + + std::map> valid_journeys; + for (const auto &pujo : records.public_journeys) { + if (pujo.key.line_planning_number == options.line_planning_number + && valid_jopas.contains(pujo.journey_pattern_code)) { + valid_journeys[pujo.key.journey_number] = { + pujo.time_demand_group_code, + pujo.journey_pattern_code, + }; + } + } + + fputs("journey_number,time_demand_group_code,journey_pattern_code\n", out); + for (const auto &[journey_number, timdemgrp_jopa] : valid_journeys) { + const auto &[time_demand_group_code, journey_pattern_code] = timdemgrp_jopa; + fprintf(out, "%d,%s,%s\n", journey_number, time_demand_group_code.c_str(), journey_pattern_code.c_str()); + } + + if (options.output_file_path != "-"sv) fclose(out); +} diff --git a/src/querykv1/journeys.hpp b/src/querykv1/journeys.hpp new file mode 100644 index 0000000..cf615c7 --- /dev/null +++ b/src/querykv1/journeys.hpp @@ -0,0 +1,13 @@ +// vim:set sw=2 ts=2 sts et: + +#ifndef OEUF_QUERYKV1_JOURNEYS_HPP +#define OEUF_QUERYKV1_JOURNEYS_HPP + +#include +#include + +#include "cliopts.hpp" + +void journeys(const Options &options, Kv1Records &records, Kv1Index &index); + +#endif // OEUF_QUERYKV1_JOURNEYS_HPP diff --git a/src/querykv1/main.cpp b/src/querykv1/main.cpp new file mode 100644 index 0000000..6c606ba --- /dev/null +++ b/src/querykv1/main.cpp @@ -0,0 +1,198 @@ +// vim:set sw=2 ts=2 sts et: + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "cliopts.hpp" +#include "joparoute.hpp" +#include "journeyinfo.hpp" +#include "journeyroute.hpp" +#include "journeys.hpp" +#include "schedule.hpp" + +using namespace std::string_view_literals; + +using TimingClock = std::conditional_t< + std::chrono::high_resolution_clock::is_steady, + std::chrono::high_resolution_clock, + std::chrono::steady_clock>; + +std::string readKv1(const char *path) { + FILE *in = stdin; + if (path != "-"sv) in = fopen(path, "rb"); + else fputs("Reading KV1 from standard input\n", stderr); + if (!in) { + fprintf(stderr, "Open %s: %s\n", path, strerrordesc_np(errno)); + exit(1); + } + + char buf[4096]; + std::string data; + while (!feof(in) && !ferror(in)) { + size_t read = fread(buf, sizeof(char), 4096, in); + data.append(buf, read); + } + if (ferror(in)) { + if (path == "-"sv) + fputs("Error when reading from stdin\n", stderr); + else + fprintf(stderr, "Error reading from file \"%s\"\n", path); + exit(1); + } + fprintf(stderr, "Read %lu bytes\n", data.size()); + + if (path != "-"sv) + fclose(in); + + return data; +} + +std::vector lex(const char *path) { + std::string data = readKv1(path); + + auto start = TimingClock::now(); + Kv1Lexer lexer(data); + lexer.lex(); + auto end = TimingClock::now(); + + std::chrono::duration elapsed{end - start}; + double bytes = static_cast(data.size()) / 1'000'000; + double speed = bytes / elapsed.count(); + + if (!lexer.errors.empty()) { + fputs("Lexer reported errors:\n", stderr); + for (const auto &error : lexer.errors) + fprintf(stderr, "- %s\n", error.c_str()); + exit(1); + } + + fprintf(stderr, "Got %lu tokens\n", lexer.tokens.size()); + fprintf(stderr, "Duration: %f s\n", elapsed.count()); + fprintf(stderr, "Speed: %f MB/s\n", speed); + + return std::move(lexer.tokens); +} + +bool parse(const char *path, Kv1Records &into) { + std::vector tokens = lex(path); + + Kv1Parser parser(tokens, into); + parser.parse(); + + bool ok = true; + if (!parser.gerrors.empty()) { + ok = false; + fputs("Parser reported errors:\n", stderr); + for (const auto &error : parser.gerrors) + fprintf(stderr, "- %s\n", error.c_str()); + } + if (!parser.warns.empty()) { + fputs("Parser reported warnings:\n", stderr); + for (const auto &warn : parser.warns) + fprintf(stderr, "- %s\n", warn.c_str()); + } + + fprintf(stderr, "Parsed %lu records\n", into.size()); + + return ok; +} + +void printParsedRecords(const Kv1Records &records) { + fputs("Parsed records:\n", stderr); + fprintf(stderr, " organizational_units: %lu\n", records.organizational_units.size()); + fprintf(stderr, " higher_organizational_units: %lu\n", records.higher_organizational_units.size()); + fprintf(stderr, " user_stop_points: %lu\n", records.user_stop_points.size()); + fprintf(stderr, " user_stop_areas: %lu\n", records.user_stop_areas.size()); + fprintf(stderr, " timing_links: %lu\n", records.timing_links.size()); + fprintf(stderr, " links: %lu\n", records.links.size()); + fprintf(stderr, " lines: %lu\n", records.lines.size()); + fprintf(stderr, " destinations: %lu\n", records.destinations.size()); + fprintf(stderr, " journey_patterns: %lu\n", records.journey_patterns.size()); + fprintf(stderr, " concession_financer_relations: %lu\n", records.concession_financer_relations.size()); + fprintf(stderr, " concession_areas: %lu\n", records.concession_areas.size()); + fprintf(stderr, " financers: %lu\n", records.financers.size()); + fprintf(stderr, " journey_pattern_timing_links: %lu\n", records.journey_pattern_timing_links.size()); + fprintf(stderr, " points: %lu\n", records.points.size()); + fprintf(stderr, " point_on_links: %lu\n", records.point_on_links.size()); + fprintf(stderr, " icons: %lu\n", records.icons.size()); + fprintf(stderr, " notices: %lu\n", records.notices.size()); + fprintf(stderr, " notice_assignments: %lu\n", records.notice_assignments.size()); + fprintf(stderr, " time_demand_groups: %lu\n", records.time_demand_groups.size()); + fprintf(stderr, " time_demand_group_run_times: %lu\n", records.time_demand_group_run_times.size()); + fprintf(stderr, " period_groups: %lu\n", records.period_groups.size()); + fprintf(stderr, " specific_days: %lu\n", records.specific_days.size()); + fprintf(stderr, " timetable_versions: %lu\n", records.timetable_versions.size()); + fprintf(stderr, " public_journeys: %lu\n", records.public_journeys.size()); + fprintf(stderr, " period_group_validities: %lu\n", records.period_group_validities.size()); + fprintf(stderr, " exceptional_operating_days: %lu\n", records.exceptional_operating_days.size()); + fprintf(stderr, " schedule_versions: %lu\n", records.schedule_versions.size()); + fprintf(stderr, " public_journey_passing_times: %lu\n", records.public_journey_passing_times.size()); + fprintf(stderr, " operating_days: %lu\n", records.operating_days.size()); +} + +void printIndexSize(const Kv1Index &index) { + fputs("Index size:\n", stderr); + fprintf(stderr, " organizational_units: %lu\n", index.organizational_units.size()); + fprintf(stderr, " user_stop_points: %lu\n", index.user_stop_points.size()); + fprintf(stderr, " user_stop_areas: %lu\n", index.user_stop_areas.size()); + fprintf(stderr, " timing_links: %lu\n", index.timing_links.size()); + fprintf(stderr, " links: %lu\n", index.links.size()); + fprintf(stderr, " lines: %lu\n", index.lines.size()); + fprintf(stderr, " destinations: %lu\n", index.destinations.size()); + fprintf(stderr, " journey_patterns: %lu\n", index.journey_patterns.size()); + fprintf(stderr, " concession_financer_relations: %lu\n", index.concession_financer_relations.size()); + fprintf(stderr, " concession_areas: %lu\n", index.concession_areas.size()); + fprintf(stderr, " financers: %lu\n", index.financers.size()); + fprintf(stderr, " journey_pattern_timing_links: %lu\n", index.journey_pattern_timing_links.size()); + fprintf(stderr, " points: %lu\n", index.points.size()); + fprintf(stderr, " point_on_links: %lu\n", index.point_on_links.size()); + fprintf(stderr, " icons: %lu\n", index.icons.size()); + fprintf(stderr, " notices: %lu\n", index.notices.size()); + fprintf(stderr, " time_demand_groups: %lu\n", index.time_demand_groups.size()); + fprintf(stderr, " time_demand_group_run_times: %lu\n", index.time_demand_group_run_times.size()); + fprintf(stderr, " period_groups: %lu\n", index.period_groups.size()); + fprintf(stderr, " specific_days: %lu\n", index.specific_days.size()); + fprintf(stderr, " timetable_versions: %lu\n", index.timetable_versions.size()); + fprintf(stderr, " public_journeys: %lu\n", index.public_journeys.size()); + fprintf(stderr, " period_group_validities: %lu\n", index.period_group_validities.size()); + fprintf(stderr, " exceptional_operating_days: %lu\n", index.exceptional_operating_days.size()); + fprintf(stderr, " schedule_versions: %lu\n", index.schedule_versions.size()); + fprintf(stderr, " public_journey_passing_times: %lu\n", index.public_journey_passing_times.size()); + fprintf(stderr, " operating_days: %lu\n", index.operating_days.size()); +} + +int main(int argc, char *argv[]) { + Options options = parseOptions(argc, argv); + + Kv1Records records; + if (!parse(options.kv1_file_path, records)) { + fputs("Error parsing records, exiting\n", stderr); + return EXIT_FAILURE; + } + printParsedRecords(records); + fputs("Indexing...\n", stderr); + Kv1Index index(&records); + fprintf(stderr, "Indexed %lu records\n", index.size()); + // Only notice assignments are not indexed. If this equality is not valid, + // then this means that we had duplicate keys or that something else went + // wrong. That would really not be great. + assert(index.size() == records.size() - records.notice_assignments.size()); + printIndexSize(index); + fputs("Linking records...\n", stderr); + kv1LinkRecords(index); + fputs("Done linking\n", stderr); + + if (options.subcommand == "joparoute"sv) jopaRoute(options, records, index); + if (options.subcommand == "journeyroute"sv) journeyRoute(options, records, index); + if (options.subcommand == "journeys"sv) journeys(options, records, index); + if (options.subcommand == "journeyinfo"sv) journeyInfo(options, records, index); + if (options.subcommand == "schedule"sv) schedule(options, records, index); +} diff --git a/src/querykv1/schedule.cpp b/src/querykv1/schedule.cpp new file mode 100644 index 0000000..2bcfe0a --- /dev/null +++ b/src/querykv1/schedule.cpp @@ -0,0 +1,63 @@ +// vim:set sw=2 ts=2 sts et: + +#include +#include +#include +#include + +#include "daterange.hpp" +#include "schedule.hpp" + +using namespace std::string_view_literals; + +void schedule(const Options &options, Kv1Records &records, Kv1Index &index) { + FILE *out = stdout; + if (options.output_file_path != "-"sv) + out = fopen(options.output_file_path, "wb"); + if (!out) { + fprintf(stderr, "Open %s: %s\n", options.output_file_path, strerrordesc_np(errno)); + exit(EXIT_FAILURE); + } + + std::cerr << "Generating schedule for " << options.line_planning_number << std::endl; + + std::unordered_multimap period_group_validities; + for (const auto &pegr : records.period_group_validities) + period_group_validities.insert({ pegr.key.period_group_code, pegr }); + std::unordered_multimap public_journeys; + for (const auto &pujo : records.public_journeys) + public_journeys.insert({ pujo.key.timetable_version_code, pujo }); + + std::cout << "line_planning_number,journey_number,date,departure_time" << std::endl; + for (const auto &tive : records.timetable_versions) { + std::vector tive_pegrval_ranges; + + auto pegrval_range = period_group_validities.equal_range(tive.key.period_group_code); + for (auto it = pegrval_range.first; it != pegrval_range.second; it++) { + const auto &[_, pegrval] = *it; + tive_pegrval_ranges.emplace_back(pegrval.key.valid_from, pegrval.valid_thru); + } + + DateRangeSeq seq(tive_pegrval_ranges.begin(), tive_pegrval_ranges.end()); + seq = seq.clampFrom(tive.valid_from); + if (tive.valid_thru) + seq = seq.clampThru(*tive.valid_thru); + + for (const auto &range : seq) for (auto date : range) { + auto weekday = std::chrono::year_month_weekday(std::chrono::sys_days(date)).weekday(); + + auto pujo_range = public_journeys.equal_range(tive.key.timetable_version_code); + for (auto itt = pujo_range.first; itt != pujo_range.second; itt++) { + const auto &[_, pujo] = *itt; + + if (pujo.key.line_planning_number == options.line_planning_number && pujo.key.day_type.size() == 7 + && pujo.key.day_type[weekday.iso_encoding() - 1] == static_cast('0' + weekday.iso_encoding())) { + std::cout << pujo.key.line_planning_number << "," << pujo.key.journey_number << "," + << date << "," << pujo.departure_time << std::endl; + } + } + } + } + + if (options.output_file_path != "-"sv) fclose(out); +} diff --git a/src/querykv1/schedule.hpp b/src/querykv1/schedule.hpp new file mode 100644 index 0000000..100bd4c --- /dev/null +++ b/src/querykv1/schedule.hpp @@ -0,0 +1,13 @@ +// vim:set sw=2 ts=2 sts et: + +#ifndef OEUF_QUERYKV1_SCHEDULE_HPP +#define OEUF_QUERYKV1_SCHEDULE_HPP + +#include +#include + +#include "cliopts.hpp" + +void schedule(const Options &options, Kv1Records &records, Kv1Index &index); + +#endif // OEUF_QUERYKV1_SCHEDULE_HPP -- cgit v1.2.3