diff options
Diffstat (limited to 'src/querykv1')
| -rw-r--r-- | src/querykv1/.envrc | 2 | ||||
| -rw-r--r-- | src/querykv1/.gitignore | 1 | ||||
| -rw-r--r-- | src/querykv1/Makefile | 28 | ||||
| -rw-r--r-- | src/querykv1/cliopts.cpp | 456 | ||||
| -rw-r--r-- | src/querykv1/cliopts.hpp | 35 | ||||
| -rw-r--r-- | src/querykv1/daterange.cpp | 91 | ||||
| -rw-r--r-- | src/querykv1/daterange.hpp | 118 | ||||
| -rw-r--r-- | src/querykv1/grammar.abnf | 44 | ||||
| -rw-r--r-- | src/querykv1/grammar.ebnf | 47 | ||||
| -rw-r--r-- | src/querykv1/grammar.ebnf.bak | 23 | ||||
| -rw-r--r-- | src/querykv1/joparoute.cpp | 102 | ||||
| -rw-r--r-- | src/querykv1/joparoute.hpp | 13 | ||||
| -rw-r--r-- | src/querykv1/journeyinfo.cpp | 64 | ||||
| -rw-r--r-- | src/querykv1/journeyinfo.hpp | 13 | ||||
| -rw-r--r-- | src/querykv1/journeyroute.cpp | 96 | ||||
| -rw-r--r-- | src/querykv1/journeyroute.hpp | 13 | ||||
| -rw-r--r-- | src/querykv1/journeys.cpp | 95 | ||||
| -rw-r--r-- | src/querykv1/journeys.hpp | 13 | ||||
| -rw-r--r-- | src/querykv1/main.cpp | 198 | ||||
| -rw-r--r-- | src/querykv1/schedule.cpp | 63 | ||||
| -rw-r--r-- | src/querykv1/schedule.hpp | 13 |
21 files changed, 1528 insertions, 0 deletions
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 @@ | |||
| 1 | source_env ../../ | ||
| 2 | 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 @@ | |||
| 1 | # Taken from: | ||
| 2 | # Open Source Security Foundation (OpenSSF), “Compiler Options Hardening Guide | ||
| 3 | # for C and C++,” OpenSSF Best Practices Working Group. Accessed: Dec. 01, | ||
| 4 | # 2023. [Online]. Available: | ||
| 5 | # https://best.openssf.org/Compiler-Hardening-Guides/Compiler-Options-Hardening-Guide-for-C-and-C++.html | ||
| 6 | CXXFLAGS=-std=c++2b -g -fno-omit-frame-pointer $(if $(DEVMODE),-Werror,)\ | ||
| 7 | -O2 -Wall -Wformat=2 -Wconversion -Wtrampolines -Wimplicit-fallthrough \ | ||
| 8 | -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 \ | ||
| 9 | -D_GLIBCXX_ASSERTIONS \ | ||
| 10 | -fstrict-flex-arrays=3 \ | ||
| 11 | -fstack-clash-protection -fstack-protector-strong | ||
| 12 | LDFLAGS=-ltmi8 -Wl,-z,defs \ | ||
| 13 | -Wl,-z,nodlopen -Wl,-z,noexecstack \ | ||
| 14 | -Wl,-z,relro -Wl,-z,now | ||
| 15 | |||
| 16 | HDRS=cliopts.hpp daterange.hpp joparoute.hpp journeyinfo.hpp journeyroute.hpp journeys.hpp schedule.hpp | ||
| 17 | SRCS=main.cpp cliopts.cpp daterange.cpp joparoute.cpp journeyinfo.cpp journeyroute.cpp journeys.cpp schedule.cpp | ||
| 18 | OBJS=$(patsubst %.cpp,%.o,$(SRCS)) | ||
| 19 | |||
| 20 | %.o: %.cpp $(HDRS) | ||
| 21 | $(CXX) -c -o $@ $< $(CXXFLAGS) | ||
| 22 | |||
| 23 | querykv1: $(OBJS) | ||
| 24 | $(CXX) -fPIE -pie -o $@ $^ $(CXXFLAGS) $(LDFLAGS) | ||
| 25 | |||
| 26 | .PHONY: clean | ||
| 27 | clean: | ||
| 28 | 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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #include <cstdlib> | ||
| 4 | #include <cstdio> | ||
| 5 | #include <string> | ||
| 6 | #include <string_view> | ||
| 7 | |||
| 8 | #include <getopt.h> | ||
| 9 | |||
| 10 | #include "cliopts.hpp" | ||
| 11 | |||
| 12 | using namespace std::string_view_literals; | ||
| 13 | |||
| 14 | const char *opt_set = ""; | ||
| 15 | const char *opt_unset = nullptr; | ||
| 16 | |||
| 17 | const char help[] = R"(Usage: %1$s [OPTIONS] <COMMAND> | ||
| 18 | |||
| 19 | Global Options: | ||
| 20 | --kv1 <PATH> Path to file containing all KV1 data, '-' for stdin | ||
| 21 | -h, --help Print this help | ||
| 22 | |||
| 23 | Commands: | ||
| 24 | joparoute Generate CSV for journey pattern route | ||
| 25 | journeyinfo Print some information on a journey | ||
| 26 | journeyroute Generate CSV for journey route | ||
| 27 | journeys List journeys of a specific line going from stop A to B | ||
| 28 | schedule Generate schedule | ||
| 29 | )"; | ||
| 30 | |||
| 31 | const char joparoute_help[] = R"(Usage: %1$s joparoute --line <NUMBER> --jopa <CODE> [OPTIONS] | ||
| 32 | |||
| 33 | Options: | ||
| 34 | --line <NUMBER> Line planning number as in schedule | ||
| 35 | --jopa <CODE> Journey pattern code as in KV1 data | ||
| 36 | -o <PATH> Path of file to write to, '-' for stdout | ||
| 37 | |||
| 38 | Global Options: | ||
| 39 | --kv1 <PATH> Path to file containing all KV1 data, '-' for stdin | ||
| 40 | -h, --help Print this help | ||
| 41 | )"; | ||
| 42 | |||
| 43 | const char journeyroute_help[] = R"(Usage: %1$s journeyroute --line <NUMBER> [OPTIONS] | ||
| 44 | |||
| 45 | Options: | ||
| 46 | --line <NUMBER> Line planning number as in KV1 data | ||
| 47 | --journey <NUMBER> Journey number as in KV1 data | ||
| 48 | -o <PATH> Path of file to write to, '-' for stdout | ||
| 49 | |||
| 50 | Global Options: | ||
| 51 | --kv1 <PATH> Path to file containing all KV1 data, '-' for stdin | ||
| 52 | -h, --help Print this help | ||
| 53 | )"; | ||
| 54 | |||
| 55 | const char journeys_help[] = R"(Usage: %1$s journeys --line <NUMBER> --begin <STOP> --end <STOP> [OPTIONS] | ||
| 56 | |||
| 57 | For the --begin and --end arguments, use the following format: | ||
| 58 | --begin/--end stop:<USRSTOP CODE> | ||
| 59 | --begin/--end star:<USRSTAR CODE> | ||
| 60 | |||
| 61 | Options: | ||
| 62 | --begin <STOP> User stop code/area of stop the journey should begin at | ||
| 63 | --end <STOP> User stop code/area of stop the journey should end at | ||
| 64 | --line <NUMBER> Line planning number to filter on | ||
| 65 | -o <PATH> Path of file to write to, '-' for stdout | ||
| 66 | |||
| 67 | Global Options: | ||
| 68 | --kv1 <PATH> Path to file containing all KV1 data, '-' for stdin | ||
| 69 | -h, --help Print this help | ||
| 70 | )"; | ||
| 71 | |||
| 72 | const char journeyinfo_help[] = R"(Usage: %1$s journeyinfo --line <NUMBER> --journey <NUMBER> [OPTIONS] | ||
| 73 | |||
| 74 | Options: | ||
| 75 | --line <NUMBER> Line planning number to filter on | ||
| 76 | --journey <NUMBER> Journey number as in schedule | ||
| 77 | |||
| 78 | Global Options: | ||
| 79 | --kv1 <PATH> Path to file containing all KV1 data, '-' for stdin | ||
| 80 | -h, --help Print this help | ||
| 81 | )"; | ||
| 82 | |||
| 83 | const char schedule_help[] = R"(Usage: %1$s schedule --line <NUMBER> [OPTIONS] | ||
| 84 | |||
| 85 | Options: | ||
| 86 | --line <NUMBER> Line planning number to generate schedule for | ||
| 87 | -o <PATH> Path of file to write to, '-' for stdout | ||
| 88 | |||
| 89 | Global Options: | ||
| 90 | --kv1 <PATH> Path to file containing all KV1 data, '-' for stdin | ||
| 91 | -h, --help Print this help | ||
| 92 | )"; | ||
| 93 | |||
| 94 | void journeyRouteValidateOptions(const char *progname, Options *options) { | ||
| 95 | #define X(name, argument, long_, short_) \ | ||
| 96 | if (#name != "kv1_file_path"sv && #name != "line_planning_number"sv \ | ||
| 97 | && #name != "journey_number"sv && #name != "help"sv && #name != "output_file_path"sv) \ | ||
| 98 | if (options->name) { \ | ||
| 99 | if (long_) { \ | ||
| 100 | if (short_) fprintf(stderr, "%s: unexpected flag --%s (-%c) for journeyroute subcommand\n\n", progname, static_cast<const char *>(long_), short_); \ | ||
| 101 | else fprintf(stderr, "%s: unexpected flag --%s for journeyroute subcommand\n\n", progname, static_cast<const char *>(long_)); \ | ||
| 102 | } else if (short_) fprintf(stderr, "%s: unexpected flag -%c for journeyroute subcommand\n\n", progname, short_); \ | ||
| 103 | fprintf(stderr, journeyroute_help, progname); \ | ||
| 104 | exit(1); \ | ||
| 105 | } | ||
| 106 | LONG_OPTIONS | ||
| 107 | SHORT_OPTIONS | ||
| 108 | #undef X | ||
| 109 | |||
| 110 | if (options->positional.size() > 0) { | ||
| 111 | fprintf(stderr, "%s: unexpected positional argument(s) for journeyroute subcommand\n\n", progname); | ||
| 112 | for (auto pos : options->positional) fprintf(stderr, "opt: %s\n", pos); | ||
| 113 | fprintf(stderr, journeyroute_help, progname); | ||
| 114 | exit(1); | ||
| 115 | } | ||
| 116 | |||
| 117 | if (!options->kv1_file_path) | ||
| 118 | options->kv1_file_path = "-"; | ||
| 119 | if (!options->output_file_path) | ||
| 120 | options->output_file_path = "-"; | ||
| 121 | if (options->kv1_file_path == ""sv) { | ||
| 122 | fprintf(stderr, "%s: KV1 file path cannot be empty\n\n", progname); | ||
| 123 | fprintf(stderr, journeyroute_help, progname); | ||
| 124 | exit(1); | ||
| 125 | } | ||
| 126 | if (options->output_file_path == ""sv) { | ||
| 127 | fprintf(stderr, "%s: output file path cannot be empty\n\n", progname); | ||
| 128 | fprintf(stderr, journeyroute_help, progname); | ||
| 129 | exit(1); | ||
| 130 | } | ||
| 131 | if (!options->journey_number || options->journey_number == ""sv) { | ||
| 132 | fprintf(stderr, "%s: journey number must be provided\n\n", progname); | ||
| 133 | fprintf(stderr, journeyroute_help, progname); | ||
| 134 | exit(1); | ||
| 135 | } | ||
| 136 | if (!options->line_planning_number || options->line_planning_number == ""sv) { | ||
| 137 | fprintf(stderr, "%s: line planning number must be provided\n\n", progname); | ||
| 138 | fprintf(stderr, journeyroute_help, progname); | ||
| 139 | exit(1); | ||
| 140 | } | ||
| 141 | } | ||
| 142 | |||
| 143 | void scheduleValidateOptions(const char *progname, Options *options) { | ||
| 144 | #define X(name, argument, long_, short_) \ | ||
| 145 | if (#name != "kv1_file_path"sv && #name != "help"sv \ | ||
| 146 | && #name != "line_planning_number"sv && #name != "output_file_path"sv) \ | ||
| 147 | if (options->name) { \ | ||
| 148 | if (long_) { \ | ||
| 149 | if (short_) fprintf(stderr, "%s: unexpected flag --%s (-%c) for schedule subcommand\n\n", progname, static_cast<const char *>(long_), short_); \ | ||
| 150 | else fprintf(stderr, "%s: unexpected flag --%s for schedule subcommand\n\n", progname, static_cast<const char *>(long_)); \ | ||
| 151 | } else if (short_) fprintf(stderr, "%s: unexpected flag -%c for schedule subcommand\n\n", progname, short_); \ | ||
| 152 | fprintf(stderr, schedule_help, progname); \ | ||
| 153 | exit(1); \ | ||
| 154 | } | ||
| 155 | LONG_OPTIONS | ||
| 156 | SHORT_OPTIONS | ||
| 157 | #undef X | ||
| 158 | |||
| 159 | if (options->positional.size() > 0) { | ||
| 160 | fprintf(stderr, "%s: unexpected positional argument(s) for schedule subcommand\n\n", progname); | ||
| 161 | for (auto pos : options->positional) fprintf(stderr, "opt: %s\n", pos); | ||
| 162 | fprintf(stderr, schedule_help, progname); | ||
| 163 | exit(1); | ||
| 164 | } | ||
| 165 | |||
| 166 | if (!options->kv1_file_path) | ||
| 167 | options->kv1_file_path = "-"; | ||
| 168 | if (!options->output_file_path) | ||
| 169 | options->output_file_path = "-"; | ||
| 170 | if (options->kv1_file_path == ""sv) { | ||
| 171 | fprintf(stderr, "%s: KV1 file path cannot be empty\n\n", progname); | ||
| 172 | fprintf(stderr, schedule_help, progname); | ||
| 173 | exit(1); | ||
| 174 | } | ||
| 175 | if (options->output_file_path == ""sv) { | ||
| 176 | fprintf(stderr, "%s: output file path cannot be empty\n\n", progname); | ||
| 177 | fprintf(stderr, schedule_help, progname); | ||
| 178 | exit(1); | ||
| 179 | } | ||
| 180 | if (!options->line_planning_number || options->line_planning_number == ""sv) { | ||
| 181 | fprintf(stderr, "%s: line planning number must be provided\n\n", progname); | ||
| 182 | fprintf(stderr, schedule_help, progname); | ||
| 183 | exit(1); | ||
| 184 | } | ||
| 185 | } | ||
| 186 | |||
| 187 | void journeysValidateOptions(const char *progname, Options *options) { | ||
| 188 | #define X(name, argument, long_, short_) \ | ||
| 189 | if (#name != "kv1_file_path"sv && #name != "help"sv \ | ||
| 190 | && #name != "line_planning_number"sv && #name != "output_file_path"sv \ | ||
| 191 | && #name != "begin_stop_code"sv && #name != "end_stop_code"sv) \ | ||
| 192 | if (options->name) { \ | ||
| 193 | if (long_) { \ | ||
| 194 | if (short_) fprintf(stderr, "%s: unexpected flag --%s (-%c) for journeys subcommand\n\n", progname, static_cast<const char *>(long_), short_); \ | ||
| 195 | else fprintf(stderr, "%s: unexpected flag --%s for journeys subcommand\n\n", progname, static_cast<const char *>(long_)); \ | ||
| 196 | } else if (short_) fprintf(stderr, "%s: unexpected flag -%c for journeys subcommand\n\n", progname, short_); \ | ||
| 197 | fprintf(stderr, journeys_help, progname); \ | ||
| 198 | exit(1); \ | ||
| 199 | } | ||
| 200 | LONG_OPTIONS | ||
| 201 | SHORT_OPTIONS | ||
| 202 | #undef X | ||
| 203 | |||
| 204 | if (options->positional.size() > 0) { | ||
| 205 | fprintf(stderr, "%s: unexpected positional argument(s) for journeys subcommand\n\n", progname); | ||
| 206 | for (auto pos : options->positional) fprintf(stderr, "opt: %s\n", pos); | ||
| 207 | fprintf(stderr, journeys_help, progname); | ||
| 208 | exit(1); | ||
| 209 | } | ||
| 210 | |||
| 211 | if (!options->kv1_file_path) | ||
| 212 | options->kv1_file_path = "-"; | ||
| 213 | if (!options->output_file_path) | ||
| 214 | options->output_file_path = "-"; | ||
| 215 | if (options->kv1_file_path == ""sv) { | ||
| 216 | fprintf(stderr, "%s: KV1 file path cannot be empty\n\n", progname); | ||
| 217 | fprintf(stderr, journeys_help, progname); | ||
| 218 | exit(1); | ||
| 219 | } | ||
| 220 | if (options->output_file_path == ""sv) { | ||
| 221 | fprintf(stderr, "%s: output file path cannot be empty\n\n", progname); | ||
| 222 | fprintf(stderr, journeys_help, progname); | ||
| 223 | exit(1); | ||
| 224 | } | ||
| 225 | if (!options->line_planning_number || options->line_planning_number == ""sv) { | ||
| 226 | fprintf(stderr, "%s: line planning number must be provided\n\n", progname); | ||
| 227 | fprintf(stderr, journeys_help, progname); | ||
| 228 | exit(1); | ||
| 229 | } | ||
| 230 | if (!options->begin_stop_code || options->begin_stop_code == ""sv) { | ||
| 231 | fprintf(stderr, "%s: start user stop code must be provided\n\n", progname); | ||
| 232 | fprintf(stderr, journeys_help, progname); | ||
| 233 | exit(1); | ||
| 234 | } | ||
| 235 | if (!options->end_stop_code || options->end_stop_code == ""sv) { | ||
| 236 | fprintf(stderr, "%s: end user stop code must be provided\n\n", progname); | ||
| 237 | fprintf(stderr, journeys_help, progname); | ||
| 238 | exit(1); | ||
| 239 | } | ||
| 240 | if (!std::string_view(options->begin_stop_code).starts_with("star:") | ||
| 241 | && !std::string_view(options->begin_stop_code).starts_with("stop:")) { | ||
| 242 | fprintf(stderr, "%s: begin user stop code must be prefixed with star:/stop:\n\n", progname); | ||
| 243 | fprintf(stderr, journeys_help, progname); | ||
| 244 | exit(1); | ||
| 245 | } | ||
| 246 | if (!std::string_view(options->end_stop_code).starts_with("star:") | ||
| 247 | && !std::string_view(options->end_stop_code).starts_with("stop:")) { | ||
| 248 | fprintf(stderr, "%s: end user stop code must be prefixed with star:/stop:\n\n", progname); | ||
| 249 | fprintf(stderr, journeys_help, progname); | ||
| 250 | exit(1); | ||
| 251 | } | ||
| 252 | } | ||
| 253 | |||
| 254 | void journeyInfoValidateOptions(const char *progname, Options *options) { | ||
| 255 | #define X(name, argument, long_, short_) \ | ||
| 256 | if (#name != "kv1_file_path"sv && #name != "line_planning_number"sv \ | ||
| 257 | && #name != "journey_number"sv && #name != "help"sv) \ | ||
| 258 | if (options->name) { \ | ||
| 259 | if (long_) { \ | ||
| 260 | if (short_) fprintf(stderr, "%s: unexpected flag --%s (-%c) for journeyinfo subcommand\n\n", progname, static_cast<const char *>(long_), short_); \ | ||
| 261 | else fprintf(stderr, "%s: unexpected flag --%s for journeyinfo subcommand\n\n", progname, static_cast<const char *>(long_)); \ | ||
| 262 | } else if (short_) fprintf(stderr, "%s: unexpected flag -%c for journeyinfo subcommand\n\n", progname, short_); \ | ||
| 263 | fprintf(stderr, journeyinfo_help, progname); \ | ||
| 264 | exit(1); \ | ||
| 265 | } | ||
| 266 | LONG_OPTIONS | ||
| 267 | SHORT_OPTIONS | ||
| 268 | #undef X | ||
| 269 | |||
| 270 | if (options->positional.size() > 0) { | ||
| 271 | fprintf(stderr, "%s: unexpected positional argument(s) for journeyinfo subcommand\n\n", progname); | ||
| 272 | for (auto pos : options->positional) fprintf(stderr, "opt: %s\n", pos); | ||
| 273 | fprintf(stderr, journeyinfo_help, progname); | ||
| 274 | exit(1); | ||
| 275 | } | ||
| 276 | |||
| 277 | if (!options->kv1_file_path) | ||
| 278 | options->kv1_file_path = "-"; | ||
| 279 | if (options->kv1_file_path == ""sv) { | ||
| 280 | fprintf(stderr, "%s: KV1 file path cannot be empty\n\n", progname); | ||
| 281 | fprintf(stderr, journeyinfo_help, progname); | ||
| 282 | exit(1); | ||
| 283 | } | ||
| 284 | if (!options->journey_number || options->journey_number == ""sv) { | ||
| 285 | fprintf(stderr, "%s: journey number must be provided\n\n", progname); | ||
| 286 | fprintf(stderr, journeyinfo_help, progname); | ||
| 287 | exit(1); | ||
| 288 | } | ||
| 289 | if (!options->line_planning_number || options->line_planning_number == ""sv) { | ||
| 290 | fprintf(stderr, "%s: line planning number must be provided\n\n", progname); | ||
| 291 | fprintf(stderr, journeyinfo_help, progname); | ||
| 292 | exit(1); | ||
| 293 | } | ||
| 294 | } | ||
| 295 | |||
| 296 | void jopaRouteValidateOptions(const char *progname, Options *options) { | ||
| 297 | #define X(name, argument, long_, short_) \ | ||
| 298 | if (#name != "kv1_file_path"sv && #name != "line_planning_number"sv \ | ||
| 299 | && #name != "journey_pattern_code"sv && #name != "help"sv && #name != "output_file_path"sv) \ | ||
| 300 | if (options->name) { \ | ||
| 301 | if (long_) { \ | ||
| 302 | if (short_) fprintf(stderr, "%s: unexpected flag --%s (-%c) for joparoute subcommand\n\n", progname, static_cast<const char *>(long_), short_); \ | ||
| 303 | else fprintf(stderr, "%s: unexpected flag --%s for joparoute subcommand\n\n", progname, static_cast<const char *>(long_)); \ | ||
| 304 | } else if (short_) fprintf(stderr, "%s: unexpected flag -%c for joparoute subcommand\n\n", progname, short_); \ | ||
| 305 | fprintf(stderr, joparoute_help, progname); \ | ||
| 306 | exit(1); \ | ||
| 307 | } | ||
| 308 | LONG_OPTIONS | ||
| 309 | SHORT_OPTIONS | ||
| 310 | #undef X | ||
| 311 | |||
| 312 | if (options->positional.size() > 0) { | ||
| 313 | fprintf(stderr, "%s: unexpected positional argument(s) for joparoute subcommand\n\n", progname); | ||
| 314 | for (auto pos : options->positional) fprintf(stderr, "opt: %s\n", pos); | ||
| 315 | fprintf(stderr, joparoute_help, progname); | ||
| 316 | exit(1); | ||
| 317 | } | ||
| 318 | |||
| 319 | if (!options->kv1_file_path) | ||
| 320 | options->kv1_file_path = "-"; | ||
| 321 | if (!options->output_file_path) | ||
| 322 | options->output_file_path = "-"; | ||
| 323 | if (options->kv1_file_path == ""sv) { | ||
| 324 | fprintf(stderr, "%s: KV1 file path cannot be empty\n\n", progname); | ||
| 325 | fprintf(stderr, joparoute_help, progname); | ||
| 326 | exit(1); | ||
| 327 | } | ||
| 328 | if (options->output_file_path == ""sv) { | ||
| 329 | fprintf(stderr, "%s: output file path cannot be empty\n\n", progname); | ||
| 330 | fprintf(stderr, joparoute_help, progname); | ||
| 331 | exit(1); | ||
| 332 | } | ||
| 333 | if (!options->journey_pattern_code || options->journey_pattern_code == ""sv) { | ||
| 334 | fprintf(stderr, "%s: journey pattern code must be provided\n\n", progname); | ||
| 335 | fprintf(stderr, joparoute_help, progname); | ||
| 336 | exit(1); | ||
| 337 | } | ||
| 338 | if (!options->line_planning_number || options->line_planning_number == ""sv) { | ||
| 339 | fprintf(stderr, "%s: line planning number must be provided\n\n", progname); | ||
| 340 | fprintf(stderr, joparoute_help, progname); | ||
| 341 | exit(1); | ||
| 342 | } | ||
| 343 | } | ||
| 344 | |||
| 345 | struct ShortFlag { | ||
| 346 | int has_arg; | ||
| 347 | int c; | ||
| 348 | }; | ||
| 349 | |||
| 350 | template<ShortFlag ...flags> | ||
| 351 | const std::string mkargarr = | ||
| 352 | (std::string() | ||
| 353 | + ... | ||
| 354 | + (flags.c == 0 | ||
| 355 | ? "" | ||
| 356 | : std::string((const char[]){ flags.c, '\0' }) | ||
| 357 | + (flags.has_arg == required_argument | ||
| 358 | ? ":" | ||
| 359 | : flags.has_arg == optional_argument | ||
| 360 | ? "::" | ||
| 361 | : ""))); | ||
| 362 | |||
| 363 | #define X(name, has_arg, long_, short_) ShortFlag(has_arg, short_), | ||
| 364 | const std::string argarr = mkargarr<SHORT_OPTIONS LONG_OPTIONS ShortFlag(no_argument, 0)>; | ||
| 365 | #undef X | ||
| 366 | |||
| 367 | Options parseOptions(int argc, char *argv[]) { | ||
| 368 | const char *progname = argv[0]; | ||
| 369 | |||
| 370 | // Struct with options for augmentkv6. | ||
| 371 | Options options; | ||
| 372 | |||
| 373 | static option long_options[] = { | ||
| 374 | #define X(name, argument, long_, short_) { long_, argument, nullptr, short_ }, | ||
| 375 | LONG_OPTIONS | ||
| 376 | #undef X | ||
| 377 | { 0 }, | ||
| 378 | }; | ||
| 379 | |||
| 380 | int c; | ||
| 381 | int option_index = 0; | ||
| 382 | bool error = false; | ||
| 383 | while ((c = getopt_long(argc, argv, argarr.c_str(), long_options, &option_index)) != -1) { | ||
| 384 | // If a long option was used, c corresponds with val. We have val = 0 for | ||
| 385 | // options which have no short alternative, so checking for c = 0 gives us | ||
| 386 | // whether a long option with no short alternative was used. | ||
| 387 | // Below, we check for c = 'h', which corresponds with the long option | ||
| 388 | // '--help', for which val = 'h'. | ||
| 389 | if (c == 0) { | ||
| 390 | const char *name = long_options[option_index].name; | ||
| 391 | #define X(opt_name, opt_has_arg, opt_long, opt_short) \ | ||
| 392 | if (name == opt_long ## sv) { options.opt_name = optarg; continue; } | ||
| 393 | LONG_OPTIONS | ||
| 394 | #undef X | ||
| 395 | error = true; | ||
| 396 | } | ||
| 397 | #define X(opt_name, opt_has_arg, opt_long, opt_short) \ | ||
| 398 | if (c == opt_short) { options.opt_name = optarg ? optarg : opt_set; continue; } | ||
| 399 | LONG_OPTIONS | ||
| 400 | SHORT_OPTIONS | ||
| 401 | #undef X | ||
| 402 | error = true; | ||
| 403 | } | ||
| 404 | |||
| 405 | if (optind < argc) | ||
| 406 | options.subcommand = argv[optind++]; | ||
| 407 | while (optind < argc) | ||
| 408 | options.positional.push_back(argv[optind++]); | ||
| 409 | |||
| 410 | if (options.subcommand | ||
| 411 | && options.subcommand != "schedule"sv | ||
| 412 | && options.subcommand != "joparoute"sv | ||
| 413 | && options.subcommand != "journeyinfo"sv | ||
| 414 | && options.subcommand != "journeyroute"sv | ||
| 415 | && options.subcommand != "journeys"sv) { | ||
| 416 | fprintf(stderr, "%s: unknown subcommand '%s'\n\n", progname, options.subcommand); | ||
| 417 | fprintf(stderr, help, progname); | ||
| 418 | exit(1); | ||
| 419 | } | ||
| 420 | if (options.subcommand && error) { | ||
| 421 | fputc('\n', stderr); | ||
| 422 | if (options.subcommand == "joparoute"sv) fprintf(stderr, joparoute_help, progname); | ||
| 423 | if (options.subcommand == "journeyinfo"sv) fprintf(stderr, journeyinfo_help, progname); | ||
| 424 | if (options.subcommand == "journeyroute"sv) fprintf(stderr, journeyroute_help, progname); | ||
| 425 | if (options.subcommand == "journeys"sv) fprintf(stderr, journeys_help, progname); | ||
| 426 | if (options.subcommand == "schedule"sv) fprintf(stderr, schedule_help, progname); | ||
| 427 | exit(1); | ||
| 428 | } | ||
| 429 | if (error || !options.subcommand) { | ||
| 430 | if (!options.subcommand) fprintf(stderr, "%s: no subcommand provided\n", progname); | ||
| 431 | fputc('\n', stderr); | ||
| 432 | fprintf(stderr, help, progname); | ||
| 433 | exit(1); | ||
| 434 | } | ||
| 435 | if (options.help) { | ||
| 436 | if (options.subcommand == "joparoute"sv) fprintf(stderr, joparoute_help, progname); | ||
| 437 | if (options.subcommand == "journeyinfo"sv) fprintf(stderr, journeyinfo_help, progname); | ||
| 438 | if (options.subcommand == "journeyroute"sv) fprintf(stderr, journeyroute_help, progname); | ||
| 439 | if (options.subcommand == "journeys"sv) fprintf(stderr, journeys_help, progname); | ||
| 440 | if (options.subcommand == "schedule"sv) fprintf(stderr, schedule_help, progname); | ||
| 441 | exit(0); | ||
| 442 | } | ||
| 443 | |||
| 444 | if (options.subcommand == "joparoute"sv) | ||
| 445 | jopaRouteValidateOptions(progname, &options); | ||
| 446 | if (options.subcommand == "journeyinfo"sv) | ||
| 447 | journeyInfoValidateOptions(progname, &options); | ||
| 448 | if (options.subcommand == "journeyroute"sv) | ||
| 449 | journeyRouteValidateOptions(progname, &options); | ||
| 450 | if (options.subcommand == "journeys"sv) | ||
| 451 | journeysValidateOptions(progname, &options); | ||
| 452 | if (options.subcommand == "schedule"sv) | ||
| 453 | scheduleValidateOptions(progname, &options); | ||
| 454 | |||
| 455 | return options; | ||
| 456 | } | ||
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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #ifndef OEUF_QUERYKV1_CLIOPTS_HPP | ||
| 4 | #define OEUF_QUERYKV1_CLIOPTS_HPP | ||
| 5 | |||
| 6 | #include <vector> | ||
| 7 | |||
| 8 | #define LONG_OPTIONS \ | ||
| 9 | /* name req/opt/no arg long short */ | ||
| 10 | X(kv1_file_path, required_argument, "kv1", 0 ) \ | ||
| 11 | X(line_planning_number, required_argument, "line", 0 ) \ | ||
| 12 | X(journey_number, required_argument, "journey", 0 ) \ | ||
| 13 | X(journey_pattern_code, required_argument, "jopa", 0 ) \ | ||
| 14 | X(begin_stop_code, required_argument, "begin", 0 ) \ | ||
| 15 | X(end_stop_code, required_argument, "end", 0 ) \ | ||
| 16 | X(help, no_argument, "help", 'h') | ||
| 17 | |||
| 18 | #define SHORT_OPTIONS \ | ||
| 19 | X(output_file_path, required_argument, nullptr, 'o') | ||
| 20 | |||
| 21 | struct Options { | ||
| 22 | const char *subcommand = nullptr; | ||
| 23 | std::vector<const char *> positional; | ||
| 24 | #define X(name, argument, long_, short_) const char *name = nullptr; | ||
| 25 | LONG_OPTIONS | ||
| 26 | SHORT_OPTIONS | ||
| 27 | #undef X | ||
| 28 | }; | ||
| 29 | |||
| 30 | extern const char *opt_set; | ||
| 31 | extern const char *opt_unset; | ||
| 32 | |||
| 33 | Options parseOptions(int argc, char *argv[]); | ||
| 34 | |||
| 35 | #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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #include "daterange.hpp" | ||
| 4 | |||
| 5 | static std::chrono::year_month_day nextDay(std::chrono::year_month_day ymd) { | ||
| 6 | return std::chrono::sys_days(ymd) + std::chrono::days(1); | ||
| 7 | } | ||
| 8 | |||
| 9 | // DateRange expresses the date range [from, thru]. | ||
| 10 | DateRange::Iterator &DateRange::Iterator::operator++() { | ||
| 11 | ymd_ = nextDay(ymd_); | ||
| 12 | return *this; | ||
| 13 | } | ||
| 14 | |||
| 15 | std::chrono::year_month_day DateRange::Iterator::operator*() const { | ||
| 16 | return ymd_; | ||
| 17 | } | ||
| 18 | |||
| 19 | std::chrono::year_month_day DateRange::Iterator::ymd() const { | ||
| 20 | return ymd_; | ||
| 21 | } | ||
| 22 | |||
| 23 | DateRange::Iterator::Iterator(std::chrono::year_month_day ymd) : ymd_(ymd) {} | ||
| 24 | |||
| 25 | DateRange::DateRange(std::chrono::year_month_day from, std::chrono::year_month_day thru) | ||
| 26 | : from_(from), thru_(thru) | ||
| 27 | {} | ||
| 28 | |||
| 29 | DateRange::Iterator DateRange::begin() const { | ||
| 30 | return DateRange::Iterator(from_); | ||
| 31 | } | ||
| 32 | |||
| 33 | DateRange::Iterator DateRange::end() const { | ||
| 34 | return DateRange::Iterator(nextDay(thru_)); | ||
| 35 | } | ||
| 36 | |||
| 37 | bool DateRange::valid() const { | ||
| 38 | return from_ <= thru_; | ||
| 39 | } | ||
| 40 | |||
| 41 | std::chrono::year_month_day DateRange::from() const { | ||
| 42 | return from_; | ||
| 43 | } | ||
| 44 | |||
| 45 | std::chrono::year_month_day DateRange::thru() const { | ||
| 46 | return thru_; | ||
| 47 | } | ||
| 48 | |||
| 49 | bool operator==(const DateRange::Iterator a, const DateRange::Iterator b) { | ||
| 50 | return *a == *b; | ||
| 51 | } | ||
| 52 | |||
| 53 | DateRangeSeq::DateRangeSeq(std::initializer_list<DateRange> ranges) | ||
| 54 | : DateRangeSeq(ranges.begin(), ranges.end()) | ||
| 55 | {} | ||
| 56 | |||
| 57 | DateRangeSeq DateRangeSeq::clampFrom(std::chrono::year_month_day from) const { | ||
| 58 | std::vector<DateRange> new_ranges; | ||
| 59 | new_ranges.reserve(ranges_.size()); | ||
| 60 | for (const DateRange range : ranges_) { | ||
| 61 | if (range.from() < from) { | ||
| 62 | if (range.thru() < from) | ||
| 63 | continue; | ||
| 64 | new_ranges.emplace_back(from, range.thru()); | ||
| 65 | } | ||
| 66 | new_ranges.push_back(range); | ||
| 67 | } | ||
| 68 | return DateRangeSeq(new_ranges.begin(), new_ranges.end()); | ||
| 69 | } | ||
| 70 | |||
| 71 | DateRangeSeq DateRangeSeq::clampThru(std::chrono::year_month_day thru) const { | ||
| 72 | std::vector<DateRange> new_ranges; | ||
| 73 | new_ranges.reserve(ranges_.size()); | ||
| 74 | for (const DateRange range : ranges_) { | ||
| 75 | if (range.thru() > thru) { | ||
| 76 | if (range.from() > thru) | ||
| 77 | continue; | ||
| 78 | new_ranges.emplace_back(range.from(), thru); | ||
| 79 | } | ||
| 80 | new_ranges.push_back(range); | ||
| 81 | } | ||
| 82 | return DateRangeSeq(new_ranges.begin(), new_ranges.end()); | ||
| 83 | } | ||
| 84 | |||
| 85 | std::vector<DateRange>::const_iterator DateRangeSeq::begin() const { | ||
| 86 | return ranges_.begin(); | ||
| 87 | } | ||
| 88 | |||
| 89 | std::vector<DateRange>::const_iterator DateRangeSeq::end() const { | ||
| 90 | return ranges_.end(); | ||
| 91 | } | ||
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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #ifndef OEUF_QUERYKV1_DATERANGE_HPP | ||
| 4 | #define OEUF_QUERYKV1_DATERANGE_HPP | ||
| 5 | |||
| 6 | #include <cassert> | ||
| 7 | #include <chrono> | ||
| 8 | #include <concepts> | ||
| 9 | #include <iterator> | ||
| 10 | #include <utility> | ||
| 11 | #include <vector> | ||
| 12 | |||
| 13 | // DateRange expresses the date range [from, thru]. | ||
| 14 | class DateRange { | ||
| 15 | public: | ||
| 16 | class Iterator { | ||
| 17 | friend class DateRange; | ||
| 18 | |||
| 19 | public: | ||
| 20 | Iterator &operator++(); | ||
| 21 | |||
| 22 | std::chrono::year_month_day operator*() const; | ||
| 23 | std::chrono::year_month_day ymd() const; | ||
| 24 | |||
| 25 | private: | ||
| 26 | explicit Iterator(std::chrono::year_month_day ymd); | ||
| 27 | |||
| 28 | std::chrono::year_month_day ymd_; | ||
| 29 | }; | ||
| 30 | |||
| 31 | explicit DateRange(std::chrono::year_month_day from, std::chrono::year_month_day thru); | ||
| 32 | |||
| 33 | Iterator begin() const; | ||
| 34 | Iterator end() const; | ||
| 35 | bool valid() const; | ||
| 36 | std::chrono::year_month_day from() const; | ||
| 37 | std::chrono::year_month_day thru() const; | ||
| 38 | |||
| 39 | private: | ||
| 40 | std::chrono::year_month_day from_; | ||
| 41 | std::chrono::year_month_day thru_; | ||
| 42 | }; | ||
| 43 | |||
| 44 | bool operator==(const DateRange::Iterator a, const DateRange::Iterator b); | ||
| 45 | |||
| 46 | template<typename Tp, typename T> | ||
| 47 | concept DerefsTo = requires(Tp p) { | ||
| 48 | { *p } -> std::convertible_to<T>; | ||
| 49 | }; | ||
| 50 | |||
| 51 | class DateRangeSeq { | ||
| 52 | // The way LE and GE are ordered makes a difference for how the sorting | ||
| 53 | // (insertion based on lower_bound) works. Do not carelessly reorder this. | ||
| 54 | enum LeGe { | ||
| 55 | GE, // >= | ||
| 56 | LE, // <= | ||
| 57 | }; | ||
| 58 | |||
| 59 | std::vector<DateRange> ranges_; | ||
| 60 | |||
| 61 | public: | ||
| 62 | template<std::input_iterator InputIt> | ||
| 63 | requires DerefsTo<InputIt, DateRange> | ||
| 64 | explicit DateRangeSeq(InputIt begin, InputIt end) { | ||
| 65 | // We convert every inclusive date range [x, y] into (x, >=) and (y, <=) | ||
| 66 | // and put these into a list, using binary search to make sure that these | ||
| 67 | // stay ordered. We then reduce this list, removing tautological | ||
| 68 | // predicates, giving us a final list of ranges that do not overlap. | ||
| 69 | |||
| 70 | std::vector<std::pair<std::chrono::year_month_day, LeGe>> preds; | ||
| 71 | |||
| 72 | size_t n = 0; | ||
| 73 | for (auto it = begin; it != end; it++) { | ||
| 74 | auto &range = *it; | ||
| 75 | if (!range.valid()) continue; | ||
| 76 | |||
| 77 | auto a = std::make_pair(range.from(), GE); | ||
| 78 | auto b = std::make_pair(range.thru(), LE); | ||
| 79 | preds.insert(std::lower_bound(preds.begin(), preds.end(), a), a); | ||
| 80 | preds.insert(std::lower_bound(preds.begin(), preds.end(), b), b); | ||
| 81 | |||
| 82 | n++; | ||
| 83 | } | ||
| 84 | |||
| 85 | if (preds.empty()) | ||
| 86 | return; | ||
| 87 | |||
| 88 | assert(preds.size() >= 2); | ||
| 89 | assert(preds.front().second == GE); | ||
| 90 | assert(preds.back().second == LE); | ||
| 91 | |||
| 92 | std::chrono::year_month_day begin_ymd = preds[0].first; | ||
| 93 | for (size_t i = 1; i < preds.size(); i++) { | ||
| 94 | if (preds[i].second == LE && (i + 1 == preds.size() || preds[i + 1].second == GE)) { | ||
| 95 | std::chrono::year_month_day end_ymd = preds[i].first; | ||
| 96 | if (!ranges_.empty() && ranges_.back().thru() == begin_ymd) | ||
| 97 | ranges_.back() = DateRange(ranges_.back().from(), end_ymd); | ||
| 98 | else | ||
| 99 | ranges_.push_back(DateRange(begin_ymd, end_ymd)); | ||
| 100 | if (i + 1 != preds.size()) { | ||
| 101 | begin_ymd = preds[i + 1].first; | ||
| 102 | i++; | ||
| 103 | } | ||
| 104 | } | ||
| 105 | } | ||
| 106 | } | ||
| 107 | |||
| 108 | explicit DateRangeSeq(std::initializer_list<DateRange> ranges); | ||
| 109 | |||
| 110 | DateRangeSeq clampFrom(std::chrono::year_month_day from) const; | ||
| 111 | DateRangeSeq clampThru(std::chrono::year_month_day thru) const; | ||
| 112 | |||
| 113 | public: | ||
| 114 | std::vector<DateRange>::const_iterator begin() const; | ||
| 115 | std::vector<DateRange>::const_iterator end() const; | ||
| 116 | }; | ||
| 117 | |||
| 118 | #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 @@ | |||
| 1 | ; This grammar does *not* allow fields to contain LF, unless the entire content | ||
| 2 | ; of the field is quoted. The file is simply rejected otherwise. | ||
| 3 | ; I took the liberty to take some inspiration from the somewhat similar IETF RFC 4180. | ||
| 4 | |||
| 5 | document = [header NEWLINE] (comment / record / empty-line) *(NEWLINE (comment / record / empty-line)) [NEWLINE] / header | ||
| 6 | |||
| 7 | header = OPENBRACK *NOTCRLF | ||
| 8 | comment = SEMICOLON *NOTCRLF | ||
| 9 | |||
| 10 | empty-line = *WHITESPACE | ||
| 11 | |||
| 12 | record = field *(PIPE field) | ||
| 13 | field = *WHITESPACE field-data *WHITESPACE | ||
| 14 | field-data = escaped / unescaped | ||
| 15 | |||
| 16 | ; Unescaped fields are also allowed to contain double quotes, | ||
| 17 | ; they are just not interpreted in any special way. | ||
| 18 | escaped = DQUOTE *(TEXTDATA / WHITESPACE / NEWLINE / PIPE / 2DQUOTE) DQUOTE | ||
| 19 | unescaped = [TEXTDATA *(*WHITESPACE (TEXTDATA / DQUOTE))] | ||
| 20 | |||
| 21 | HTAB = %x09 ; <horizontal tab, "\t"> | ||
| 22 | LF = %x0A ; <line feed, "\n"> | ||
| 23 | VTAB = %x0B ; <vertical tab, "\v"> | ||
| 24 | FF = %x0C ; <form feed, "\f"> | ||
| 25 | CR = %x0D ; <carriage return, "\r"> | ||
| 26 | SPACE = %x20 ; <space, " "> | ||
| 27 | DQUOTE = %x22 ; " | ||
| 28 | SEMICOLON = %x3B ; ; | ||
| 29 | OPENBRACK = %x5B ; [ | ||
| 30 | PIPE = %x7C ; | | ||
| 31 | |||
| 32 | ; All codepoints, except CR, LF, SPACE, FF, HTAB, VTAB, PIPE, DQUOTE | ||
| 33 | ; Semicolon is included, as comments are only defined as 'lines starting with a semicolon'. | ||
| 34 | ; So it should be fine if a semicolon is part of a field, the rest of the line would not | ||
| 35 | ; be interpreted as a comment in that case. | ||
| 36 | TEXTDATA = %x00-08 / %x0E-1F / %x21 / %x23-5A / %x5C-7B / %x7D-10FFFF | ||
| 37 | |||
| 38 | ; Not including LF here even though TMI8/KV1 does not officially consider it | ||
| 39 | ; a newline, as newlines are defined as 'CR optionally followed by LF' | ||
| 40 | WHITESPACE = SPACE / FF / HTAB / VTAB | ||
| 41 | |||
| 42 | ; All codepoints excluding CR and LF | ||
| 43 | NOTCRLF = %x00-09 / %x0B-0C / %x0E-10FFFF | ||
| 44 | 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 @@ | |||
| 1 | /* This grammar does allow fields to contain stray LFs, not after any specific | ||
| 2 | * CR. I took the liberty to take some inspiration from the somewhat similar | ||
| 3 | * IETF RFC 4180. | ||
| 4 | */ | ||
| 5 | document ::= (header NEWLINE)? (comment | record | empty-line) (NEWLINE (comment | record | empty-line))* NEWLINE? | header | ||
| 6 | |||
| 7 | header ::= OPENBRACK NOTCR* | ||
| 8 | comment ::= SEMICOLON NOTCR* | ||
| 9 | |||
| 10 | empty-line ::= WHITESPACE* | ||
| 11 | |||
| 12 | record ::= field (PIPE field)* | ||
| 13 | field ::= WHITESPACE* field-data WHITESPACE* | ||
| 14 | field-data ::= DQUOTE escaped DQUOTE | unescaped | ||
| 15 | |||
| 16 | /* Unescaped fields are also allowed to contain double quotes, they are just | ||
| 17 | * not interpreted in any special way. | ||
| 18 | */ | ||
| 19 | escaped ::= (TEXTDATA | WHITESPACE | NEWLINE | PIPE | DQUOTE DQUOTE)* | ||
| 20 | unescaped ::= (TEXTDATA (WHITESPACE* (TEXTDATA | DQUOTE))*)? | ||
| 21 | |||
| 22 | HTAB ::= #x09 /* <horizontal tab, "\t"> */ | ||
| 23 | LF ::= #x0A /* <line feed, "\n"> */ | ||
| 24 | VTAB ::= #x0B /* <vertical tab, "\v"> */ | ||
| 25 | FF ::= #x0C /* <form feed, "\f"> */ | ||
| 26 | CR ::= #x0D /* <carriage return, "\r"> */ | ||
| 27 | SPACE ::= #x20 /* <space, " "> */ | ||
| 28 | DQUOTE ::= #x22 /* " */ | ||
| 29 | SEMICOLON ::= #x3B /* ; */ | ||
| 30 | OPENBRACK ::= #x5B /* [ */ | ||
| 31 | PIPE ::= #x7C /* | */ | ||
| 32 | |||
| 33 | /* All codepoints, except CR, LF, SPACE, FF, HTAB, VTAB, PIPE, DQUOTE. | ||
| 34 | * Semicolon is included, as comments are only defined as 'lines starting with | ||
| 35 | * a semicolon'. So it should be fine if a semicolon is part of a field, the | ||
| 36 | * rest of the line would not be interpreted as a comment in that case. | ||
| 37 | */ | ||
| 38 | TEXTDATA ::= [#x00-#x08#x0E-#x1F#x21#x23-#x5A#x5C-#x7B#x7D-#x10FFFF] | ||
| 39 | |||
| 40 | /* Including LF here as TMI8/KV1 does not consider it a newline, | ||
| 41 | * as newlines are defined as 'CR optionally followed by LF' | ||
| 42 | */ | ||
| 43 | WHITESPACE ::= SPACE | LF | FF | HTAB | VTAB | ||
| 44 | |||
| 45 | /* All codepoints excluding CR and LF */ | ||
| 46 | NOTCR ::= [#x00-#x0C#x0E-#x10FFFF] | ||
| 47 | 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 @@ | |||
| 1 | document ::= (header NEWLINE)? (comment | record | empty-line) (NEWLINE (comment | record | empty-line))* NEWLINE? | header | ||
| 2 | header ::= OPENBRACK NOTCRLF* | ||
| 3 | comment ::= SEMICOLON NOTCRLF* | ||
| 4 | empty-line ::= WHITESPACE* | ||
| 5 | record ::= field (PIPE field)* | ||
| 6 | field ::= WHITESPACE* field-data WHITESPACE* | ||
| 7 | field-data ::= escaped | unescaped | ||
| 8 | escaped ::= DQUOTE (TEXTDATA | WHITESPACE | NEWLINE | PIPE | DQUOTE DQUOTE)* DQUOTE | ||
| 9 | unescaped ::= (TEXTDATA (WHITESPACE* (TEXTDATA | DQUOTE))*)? | ||
| 10 | HTAB ::= #x09 | ||
| 11 | LF ::= #x0A | ||
| 12 | VTAB ::= #x0B | ||
| 13 | FF ::= #x0C | ||
| 14 | CR ::= #x0D | ||
| 15 | SPACE ::= #x20 | ||
| 16 | DQUOTE ::= #x22 | ||
| 17 | SEMICOLON ::= #x3B | ||
| 18 | OPENBRACK ::= #x5B | ||
| 19 | PIPE ::= #x7C | ||
| 20 | WHITESPACE ::= SPACE | FF | HTAB | VTAB | ||
| 21 | NOTCRLF ::= [#x00-#x09#x0B-#x0C#x0E-#x10FFFF] | ||
| 22 | TEXTDATA ::= [#x00-#x08#x0E-#x1F#x21#x23-#x5A#x5C-#x7B#x7D-#x10FFFF] | ||
| 23 | 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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #include <cstdio> | ||
| 4 | #include <iostream> | ||
| 5 | #include <string_view> | ||
| 6 | |||
| 7 | #include "joparoute.hpp" | ||
| 8 | |||
| 9 | using namespace std::string_view_literals; | ||
| 10 | |||
| 11 | void jopaRoute(const Options &options, Kv1Records &records, Kv1Index &index) { | ||
| 12 | FILE *out = stdout; | ||
| 13 | if (options.output_file_path != "-"sv) | ||
| 14 | out = fopen(options.output_file_path, "wb"); | ||
| 15 | if (!out) { | ||
| 16 | fprintf(stderr, "Open %s: %s\n", options.output_file_path, strerrordesc_np(errno)); | ||
| 17 | exit(EXIT_FAILURE); | ||
| 18 | } | ||
| 19 | |||
| 20 | const std::string data_owner_code = "CXX"; | ||
| 21 | Kv1JourneyPattern::Key jopa_key( | ||
| 22 | // Of course it is bad to hardcode this, but we really have no time to make | ||
| 23 | // everything nice and dynamic. We're only working with CXX data anyway, | ||
| 24 | // and provide no support for the 'Schedules and Passing Times' KV1 | ||
| 25 | // variant. | ||
| 26 | data_owner_code, | ||
| 27 | options.line_planning_number, | ||
| 28 | options.journey_pattern_code); | ||
| 29 | |||
| 30 | const Kv1JourneyPattern *jopa = index.journey_patterns[jopa_key]; | ||
| 31 | if (!jopa) { | ||
| 32 | std::cerr << "Journey pattern not found" << std::endl; | ||
| 33 | return; | ||
| 34 | } | ||
| 35 | const Kv1Line *line = jopa->p_line; | ||
| 36 | |||
| 37 | struct Point { | ||
| 38 | bool is_stop = false; | ||
| 39 | const Kv1JourneyPatternTimingLink *jopatili = nullptr; | ||
| 40 | const Kv1Link *link = nullptr; | ||
| 41 | const Kv1Point *point = nullptr; | ||
| 42 | double distance_since_start_of_link = 0; | ||
| 43 | double distance_since_start_of_journey = 0; | ||
| 44 | }; | ||
| 45 | std::vector<Point> points; | ||
| 46 | |||
| 47 | for (size_t i = 0; i < records.journey_pattern_timing_links.size(); i++) { | ||
| 48 | const Kv1JourneyPatternTimingLink *jopatili = &records.journey_pattern_timing_links[i]; | ||
| 49 | if (jopatili->key.line_planning_number == jopa->key.line_planning_number | ||
| 50 | && jopatili->key.journey_pattern_code == jopa->key.journey_pattern_code) { | ||
| 51 | const Kv1Link::Key link_key(data_owner_code, jopatili->user_stop_code_begin, | ||
| 52 | jopatili->user_stop_code_end, line->transport_type); | ||
| 53 | const Kv1Link *link = index.links[link_key]; | ||
| 54 | const Kv1UserStopPoint::Key link_begin_key(data_owner_code, jopatili->user_stop_code_begin); | ||
| 55 | const Kv1UserStopPoint::Key link_end_key(data_owner_code, jopatili->user_stop_code_end); | ||
| 56 | const Kv1UserStopPoint *link_begin = index.user_stop_points[link_begin_key]; | ||
| 57 | const Kv1UserStopPoint *link_end = index.user_stop_points[link_end_key]; | ||
| 58 | |||
| 59 | points.emplace_back(true, jopatili, link, link_begin->p_point, 0); | ||
| 60 | |||
| 61 | for (size_t j = 0; j < records.point_on_links.size(); j++) { | ||
| 62 | Kv1PointOnLink *pool = &records.point_on_links[j]; | ||
| 63 | if (pool->key.user_stop_code_begin == jopatili->user_stop_code_begin | ||
| 64 | && pool->key.user_stop_code_end == jopatili->user_stop_code_end | ||
| 65 | && pool->key.transport_type == jopatili->p_line->transport_type) { | ||
| 66 | points.emplace_back(false, jopatili, link, pool->p_point, pool->distance_since_start_of_link); | ||
| 67 | } | ||
| 68 | } | ||
| 69 | |||
| 70 | points.emplace_back(true, jopatili, link, link_end->p_point, link->distance); | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | std::sort(points.begin(), points.end(), [](Point &a, Point &b) { | ||
| 75 | if (a.jopatili->key.timing_link_order != b.jopatili->key.timing_link_order) | ||
| 76 | return a.jopatili->key.timing_link_order < b.jopatili->key.timing_link_order; | ||
| 77 | return a.distance_since_start_of_link < b.distance_since_start_of_link; | ||
| 78 | }); | ||
| 79 | |||
| 80 | double distance_since_start_of_journey = 0; | ||
| 81 | for (size_t i = 0; i < points.size(); i++) { | ||
| 82 | Point *p = &points[i]; | ||
| 83 | if (i > 0) { | ||
| 84 | Point *prev = &points[i - 1]; | ||
| 85 | if (p->link != prev->link) { | ||
| 86 | distance_since_start_of_journey += prev->link->distance; | ||
| 87 | } | ||
| 88 | } | ||
| 89 | p->distance_since_start_of_journey = distance_since_start_of_journey + p->distance_since_start_of_link; | ||
| 90 | } | ||
| 91 | |||
| 92 | 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); | ||
| 93 | for (const auto &point : points) { | ||
| 94 | fprintf(out, "%s,%s,%s,%s,%f,%f,%f,%f\n", | ||
| 95 | point.is_stop ? "true" : "false", | ||
| 96 | point.jopatili->user_stop_code_begin.c_str(), point.jopatili->user_stop_code_end.c_str(), | ||
| 97 | point.point->key.point_code.c_str(), point.point->location_x_ew, point.point->location_y_ns, | ||
| 98 | point.distance_since_start_of_link, point.distance_since_start_of_journey); | ||
| 99 | } | ||
| 100 | |||
| 101 | if (options.output_file_path != "-"sv) fclose(out); | ||
| 102 | } | ||
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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #ifndef OEUF_QUERYKV1_JOPAROUTE_HPP | ||
| 4 | #define OEUF_QUERYKV1_JOPAROUTE_HPP | ||
| 5 | |||
| 6 | #include <tmi8/kv1_types.hpp> | ||
| 7 | #include <tmi8/kv1_index.hpp> | ||
| 8 | |||
| 9 | #include "cliopts.hpp" | ||
| 10 | |||
| 11 | void jopaRoute(const Options &options, Kv1Records &records, Kv1Index &index); | ||
| 12 | |||
| 13 | #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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #include <iostream> | ||
| 4 | |||
| 5 | #include "journeyinfo.hpp" | ||
| 6 | |||
| 7 | void journeyInfo(const Options &options, Kv1Records &records, Kv1Index &index) { | ||
| 8 | std::cout << "Info for journey " << options.line_planning_number | ||
| 9 | << "/" << options.journey_number << std::endl; | ||
| 10 | |||
| 11 | std::unordered_map<std::string, const Kv1UserStopPoint *> usrstops; | ||
| 12 | for (size_t i = 0; i < records.user_stop_points.size(); i++) { | ||
| 13 | const Kv1UserStopPoint *usrstop = &records.user_stop_points[i]; | ||
| 14 | usrstops[usrstop->key.user_stop_code] = usrstop; | ||
| 15 | } | ||
| 16 | |||
| 17 | for (const auto &pujo : records.public_journeys) { | ||
| 18 | if (pujo.key.line_planning_number != options.line_planning_number | ||
| 19 | || std::to_string(pujo.key.journey_number) != options.journey_number) | ||
| 20 | continue; | ||
| 21 | |||
| 22 | std::vector<const Kv1JourneyPatternTimingLink *> timing_links; | ||
| 23 | for (size_t i = 0; i < records.journey_pattern_timing_links.size(); i++) { | ||
| 24 | const Kv1JourneyPatternTimingLink *jopatili = &records.journey_pattern_timing_links[i]; | ||
| 25 | if (jopatili->key.line_planning_number != options.line_planning_number | ||
| 26 | || jopatili->key.journey_pattern_code != pujo.journey_pattern_code) | ||
| 27 | continue; | ||
| 28 | timing_links.push_back(jopatili); | ||
| 29 | } | ||
| 30 | |||
| 31 | std::sort(timing_links.begin(), timing_links.end(), [](auto a, auto b) -> bool { | ||
| 32 | return a->key.timing_link_order < b->key.timing_link_order; | ||
| 33 | }); | ||
| 34 | auto begin_stop = timing_links.front()->user_stop_code_begin; | ||
| 35 | auto end_stop = timing_links.back()->user_stop_code_end; | ||
| 36 | |||
| 37 | const auto *begin = usrstops[begin_stop]; | ||
| 38 | const auto *end = usrstops[end_stop]; | ||
| 39 | |||
| 40 | std::cout << " Journey pattern: " << pujo.key.line_planning_number | ||
| 41 | << "/" << pujo.journey_pattern_code << std::endl | ||
| 42 | << " Begin stop: " << begin_stop | ||
| 43 | << "; name: " << std::quoted(begin->name) | ||
| 44 | << "; town: " << std::quoted(begin->town) << std::endl | ||
| 45 | << " End stop: " << end_stop | ||
| 46 | << "; name: " << std::quoted(end->name) | ||
| 47 | << "; town: " << std::quoted(end->town) << std::endl; | ||
| 48 | |||
| 49 | const auto *begin_star = begin->p_user_stop_area; | ||
| 50 | const auto *end_star = end->p_user_stop_area; | ||
| 51 | if (begin_star) | ||
| 52 | std::cout << " Begin stop area: " << begin_star->key.user_stop_area_code | ||
| 53 | << "; name: " << std::quoted(begin_star->name) | ||
| 54 | << ", town: " << std::quoted(begin_star->town) | ||
| 55 | << std::endl; | ||
| 56 | if (end_star) | ||
| 57 | std::cout << " End stop area: " << end_star->key.user_stop_area_code | ||
| 58 | << "; name: " << std::quoted(end_star->name) | ||
| 59 | << ", town: " << std::quoted(end_star->town) | ||
| 60 | << std::endl; | ||
| 61 | |||
| 62 | break; | ||
| 63 | } | ||
| 64 | } | ||
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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #ifndef OEUF_QUERYKV1_JOURNEYINFO_HPP | ||
| 4 | #define OEUF_QUERYKV1_JOURNEYINFO_HPP | ||
| 5 | |||
| 6 | #include <tmi8/kv1_types.hpp> | ||
| 7 | #include <tmi8/kv1_index.hpp> | ||
| 8 | |||
| 9 | #include "cliopts.hpp" | ||
| 10 | |||
| 11 | void journeyInfo(const Options &options, Kv1Records &records, Kv1Index &index); | ||
| 12 | |||
| 13 | #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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #include <iostream> | ||
| 4 | #include <string_view> | ||
| 5 | |||
| 6 | #include "journeyroute.hpp" | ||
| 7 | |||
| 8 | using namespace std::string_view_literals; | ||
| 9 | |||
| 10 | void journeyRoute(const Options &options, Kv1Records &records, Kv1Index &index) { | ||
| 11 | FILE *out = stdout; | ||
| 12 | if (options.output_file_path != "-"sv) | ||
| 13 | out = fopen(options.output_file_path, "wb"); | ||
| 14 | if (!out) { | ||
| 15 | fprintf(stderr, "Open %s: %s\n", options.output_file_path, strerrordesc_np(errno)); | ||
| 16 | exit(EXIT_FAILURE); | ||
| 17 | } | ||
| 18 | |||
| 19 | for (auto &pujo : records.public_journeys) { | ||
| 20 | if (pujo.key.line_planning_number == options.line_planning_number && std::to_string(pujo.key.journey_number) == options.journey_number) { | ||
| 21 | fprintf(stderr, "Got PUJO %s/%s:\n", options.line_planning_number, options.journey_number); | ||
| 22 | fprintf(stderr, " Day type: %s\n", pujo.key.day_type.c_str()); | ||
| 23 | auto &pegr = *pujo.p_period_group; | ||
| 24 | fprintf(stderr, " PEGR Code: %s\n", pegr.key.period_group_code.c_str()); | ||
| 25 | fprintf(stderr, " PEGR Description: %s\n", pegr.description.c_str()); | ||
| 26 | fprintf(stderr, " SPECDAY Code: %s\n", pujo.key.specific_day_code.c_str()); | ||
| 27 | auto &timdemgrp = *pujo.p_time_demand_group; | ||
| 28 | |||
| 29 | for (auto &pegrval : records.period_group_validities) { | ||
| 30 | if (pegrval.key.period_group_code == pegr.key.period_group_code) { | ||
| 31 | fprintf(stderr, "Got PEGRVAL for PEGR %s\n", pegr.key.period_group_code.c_str()); | ||
| 32 | std::cerr << " Valid from: " << pegrval.key.valid_from << std::endl; | ||
| 33 | std::cerr << " Valid thru: " << pegrval.valid_thru << std::endl; | ||
| 34 | } | ||
| 35 | } | ||
| 36 | |||
| 37 | struct Point { | ||
| 38 | Kv1JourneyPatternTimingLink *jopatili = nullptr; | ||
| 39 | Kv1TimeDemandGroupRunTime *timdemrnt = nullptr; | ||
| 40 | double distance_since_start_of_link = 0; | ||
| 41 | double rd_x = 0; | ||
| 42 | double rd_y = 0; | ||
| 43 | double total_time_s = 0; | ||
| 44 | }; | ||
| 45 | std::vector<Point> points; | ||
| 46 | |||
| 47 | for (size_t i = 0; i < records.time_demand_group_run_times.size(); i++) { | ||
| 48 | Kv1TimeDemandGroupRunTime *timdemrnt = &records.time_demand_group_run_times[i]; | ||
| 49 | if (timdemrnt->key.line_planning_number == timdemgrp.key.line_planning_number | ||
| 50 | && timdemrnt->key.journey_pattern_code == timdemgrp.key.journey_pattern_code | ||
| 51 | && timdemrnt->key.time_demand_group_code == timdemgrp.key.time_demand_group_code) { | ||
| 52 | Kv1JourneyPatternTimingLink *jopatili = timdemrnt->p_journey_pattern_timing_link; | ||
| 53 | for (auto &pool : records.point_on_links) { | ||
| 54 | if (pool.key.user_stop_code_begin == timdemrnt->user_stop_code_begin | ||
| 55 | && pool.key.user_stop_code_end == timdemrnt->user_stop_code_end | ||
| 56 | && pool.key.transport_type == jopatili->p_line->transport_type) { | ||
| 57 | points.emplace_back( | ||
| 58 | jopatili, | ||
| 59 | timdemrnt, | ||
| 60 | pool.distance_since_start_of_link, | ||
| 61 | pool.p_point->location_x_ew, | ||
| 62 | pool.p_point->location_y_ns | ||
| 63 | ); | ||
| 64 | } | ||
| 65 | } | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | std::sort(points.begin(), points.end(), [](Point &a, Point &b) { | ||
| 70 | if (a.jopatili->key.timing_link_order != b.jopatili->key.timing_link_order) | ||
| 71 | return a.jopatili->key.timing_link_order < b.jopatili->key.timing_link_order; | ||
| 72 | return a.distance_since_start_of_link < b.distance_since_start_of_link; | ||
| 73 | }); | ||
| 74 | |||
| 75 | double total_time_s = 0; | ||
| 76 | for (size_t i = 0; i < points.size(); i++) { | ||
| 77 | Point *p = &points[i]; | ||
| 78 | p->total_time_s = total_time_s; | ||
| 79 | if (i > 0) { | ||
| 80 | Point *prev = &points[i - 1]; | ||
| 81 | if (p->timdemrnt != prev->timdemrnt) { | ||
| 82 | total_time_s += prev->timdemrnt->total_drive_time_s; | ||
| 83 | prev->total_time_s = total_time_s; | ||
| 84 | } | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | fputs("rd_x,rd_y,total_time_s,is_timing_stop\n", out); | ||
| 89 | for (const auto &point : points) { | ||
| 90 | fprintf(out, "%f,%f,%f,%d\n", point.rd_x, point.rd_y, point.total_time_s, point.jopatili->is_timing_stop); | ||
| 91 | } | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | if (options.output_file_path != "-"sv) fclose(out); | ||
| 96 | } | ||
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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #ifndef OEUF_QUERYKV1_JOURNEYROUTE_HPP | ||
| 4 | #define OEUF_QUERYKV1_JOURNEYROUTE_HPP | ||
| 5 | |||
| 6 | #include <tmi8/kv1_types.hpp> | ||
| 7 | #include <tmi8/kv1_index.hpp> | ||
| 8 | |||
| 9 | #include "cliopts.hpp" | ||
| 10 | |||
| 11 | void journeyRoute(const Options &options, Kv1Records &records, Kv1Index &index); | ||
| 12 | |||
| 13 | #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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #include <iostream> | ||
| 4 | #include <map> | ||
| 5 | #include <string_view> | ||
| 6 | #include <unordered_set> | ||
| 7 | |||
| 8 | #include "journeys.hpp" | ||
| 9 | |||
| 10 | using namespace std::string_view_literals; | ||
| 11 | |||
| 12 | void journeys(const Options &options, Kv1Records &records, Kv1Index &index) { | ||
| 13 | const std::string_view want_begin_stop_code(options.begin_stop_code); | ||
| 14 | const std::string_view want_end_stop_code(options.end_stop_code); | ||
| 15 | |||
| 16 | FILE *out = stdout; | ||
| 17 | if (options.output_file_path != "-"sv) | ||
| 18 | out = fopen(options.output_file_path, "wb"); | ||
| 19 | if (!out) { | ||
| 20 | fprintf(stderr, "Open %s: %s\n", options.output_file_path, strerrordesc_np(errno)); | ||
| 21 | exit(EXIT_FAILURE); | ||
| 22 | } | ||
| 23 | |||
| 24 | std::cerr << "Generating journeys for " << options.line_planning_number << ", going from stop " | ||
| 25 | << options.begin_stop_code << " to " << options.end_stop_code << std::endl; | ||
| 26 | |||
| 27 | std::unordered_map<std::string, const Kv1UserStopPoint *> usrstops; | ||
| 28 | for (size_t i = 0; i < records.user_stop_points.size(); i++) { | ||
| 29 | const Kv1UserStopPoint *usrstop = &records.user_stop_points[i]; | ||
| 30 | usrstops[usrstop->key.user_stop_code] = usrstop; | ||
| 31 | } | ||
| 32 | |||
| 33 | std::unordered_set<std::string> journey_pattern_codes; | ||
| 34 | for (const auto &jopa : records.journey_patterns) { | ||
| 35 | if (jopa.key.line_planning_number != options.line_planning_number) | ||
| 36 | continue; | ||
| 37 | journey_pattern_codes.insert(jopa.key.journey_pattern_code); | ||
| 38 | } | ||
| 39 | |||
| 40 | std::unordered_map<std::string, std::vector<const Kv1JourneyPatternTimingLink *>> jopatilis; | ||
| 41 | for (size_t i = 0; i < records.journey_pattern_timing_links.size(); i++) { | ||
| 42 | const Kv1JourneyPatternTimingLink *jopatili = &records.journey_pattern_timing_links[i]; | ||
| 43 | if (jopatili->key.line_planning_number != options.line_planning_number | ||
| 44 | || !journey_pattern_codes.contains(jopatili->key.journey_pattern_code)) | ||
| 45 | continue; | ||
| 46 | jopatilis[jopatili->key.journey_pattern_code].push_back(jopatili); | ||
| 47 | } | ||
| 48 | |||
| 49 | std::unordered_set<std::string> valid_jopas; | ||
| 50 | for (auto &[journey_pattern_code, timing_links] : jopatilis) { | ||
| 51 | std::sort(timing_links.begin(), timing_links.end(), [](auto a, auto b) -> bool { | ||
| 52 | return a->key.timing_link_order < b->key.timing_link_order; | ||
| 53 | }); | ||
| 54 | auto begin_stop = timing_links.front()->user_stop_code_begin; | ||
| 55 | auto end_stop = timing_links.back()->user_stop_code_end; | ||
| 56 | |||
| 57 | const auto *begin = usrstops[begin_stop]; | ||
| 58 | const auto *end = usrstops[end_stop]; | ||
| 59 | |||
| 60 | bool begin_stop_ok = false; | ||
| 61 | if (want_begin_stop_code.starts_with("stop:")) | ||
| 62 | begin_stop_ok = want_begin_stop_code.substr(5) == begin_stop; | ||
| 63 | else if (want_begin_stop_code.starts_with("star:")) | ||
| 64 | begin_stop_ok = want_begin_stop_code.substr(5) == begin->user_stop_area_code; | ||
| 65 | |||
| 66 | bool end_stop_ok = false; | ||
| 67 | if (want_end_stop_code.starts_with("stop:")) | ||
| 68 | end_stop_ok = want_end_stop_code.substr(5) == end_stop; | ||
| 69 | else if (want_end_stop_code.starts_with("star:")) | ||
| 70 | end_stop_ok = want_end_stop_code.substr(5) == end->user_stop_area_code; | ||
| 71 | |||
| 72 | if (begin_stop_ok && end_stop_ok) { | ||
| 73 | valid_jopas.insert(journey_pattern_code); | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | std::map<int, std::pair<std::string, std::string>> valid_journeys; | ||
| 78 | for (const auto &pujo : records.public_journeys) { | ||
| 79 | if (pujo.key.line_planning_number == options.line_planning_number | ||
| 80 | && valid_jopas.contains(pujo.journey_pattern_code)) { | ||
| 81 | valid_journeys[pujo.key.journey_number] = { | ||
| 82 | pujo.time_demand_group_code, | ||
| 83 | pujo.journey_pattern_code, | ||
| 84 | }; | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | fputs("journey_number,time_demand_group_code,journey_pattern_code\n", out); | ||
| 89 | for (const auto &[journey_number, timdemgrp_jopa] : valid_journeys) { | ||
| 90 | const auto &[time_demand_group_code, journey_pattern_code] = timdemgrp_jopa; | ||
| 91 | fprintf(out, "%d,%s,%s\n", journey_number, time_demand_group_code.c_str(), journey_pattern_code.c_str()); | ||
| 92 | } | ||
| 93 | |||
| 94 | if (options.output_file_path != "-"sv) fclose(out); | ||
| 95 | } | ||
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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #ifndef OEUF_QUERYKV1_JOURNEYS_HPP | ||
| 4 | #define OEUF_QUERYKV1_JOURNEYS_HPP | ||
| 5 | |||
| 6 | #include <tmi8/kv1_types.hpp> | ||
| 7 | #include <tmi8/kv1_index.hpp> | ||
| 8 | |||
| 9 | #include "cliopts.hpp" | ||
| 10 | |||
| 11 | void journeys(const Options &options, Kv1Records &records, Kv1Index &index); | ||
| 12 | |||
| 13 | #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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #include <chrono> | ||
| 4 | #include <cstdio> | ||
| 5 | #include <string> | ||
| 6 | #include <string_view> | ||
| 7 | #include <vector> | ||
| 8 | |||
| 9 | #include <tmi8/kv1_types.hpp> | ||
| 10 | #include <tmi8/kv1_index.hpp> | ||
| 11 | #include <tmi8/kv1_lexer.hpp> | ||
| 12 | #include <tmi8/kv1_parser.hpp> | ||
| 13 | |||
| 14 | #include "cliopts.hpp" | ||
| 15 | #include "joparoute.hpp" | ||
| 16 | #include "journeyinfo.hpp" | ||
| 17 | #include "journeyroute.hpp" | ||
| 18 | #include "journeys.hpp" | ||
| 19 | #include "schedule.hpp" | ||
| 20 | |||
| 21 | using namespace std::string_view_literals; | ||
| 22 | |||
| 23 | using TimingClock = std::conditional_t< | ||
| 24 | std::chrono::high_resolution_clock::is_steady, | ||
| 25 | std::chrono::high_resolution_clock, | ||
| 26 | std::chrono::steady_clock>; | ||
| 27 | |||
| 28 | std::string readKv1(const char *path) { | ||
| 29 | FILE *in = stdin; | ||
| 30 | if (path != "-"sv) in = fopen(path, "rb"); | ||
| 31 | else fputs("Reading KV1 from standard input\n", stderr); | ||
| 32 | if (!in) { | ||
| 33 | fprintf(stderr, "Open %s: %s\n", path, strerrordesc_np(errno)); | ||
| 34 | exit(1); | ||
| 35 | } | ||
| 36 | |||
| 37 | char buf[4096]; | ||
| 38 | std::string data; | ||
| 39 | while (!feof(in) && !ferror(in)) { | ||
| 40 | size_t read = fread(buf, sizeof(char), 4096, in); | ||
| 41 | data.append(buf, read); | ||
| 42 | } | ||
| 43 | if (ferror(in)) { | ||
| 44 | if (path == "-"sv) | ||
| 45 | fputs("Error when reading from stdin\n", stderr); | ||
| 46 | else | ||
| 47 | fprintf(stderr, "Error reading from file \"%s\"\n", path); | ||
| 48 | exit(1); | ||
| 49 | } | ||
| 50 | fprintf(stderr, "Read %lu bytes\n", data.size()); | ||
| 51 | |||
| 52 | if (path != "-"sv) | ||
| 53 | fclose(in); | ||
| 54 | |||
| 55 | return data; | ||
| 56 | } | ||
| 57 | |||
| 58 | std::vector<Kv1Token> lex(const char *path) { | ||
| 59 | std::string data = readKv1(path); | ||
| 60 | |||
| 61 | auto start = TimingClock::now(); | ||
| 62 | Kv1Lexer lexer(data); | ||
| 63 | lexer.lex(); | ||
| 64 | auto end = TimingClock::now(); | ||
| 65 | |||
| 66 | std::chrono::duration<double> elapsed{end - start}; | ||
| 67 | double bytes = static_cast<double>(data.size()) / 1'000'000; | ||
| 68 | double speed = bytes / elapsed.count(); | ||
| 69 | |||
| 70 | if (!lexer.errors.empty()) { | ||
| 71 | fputs("Lexer reported errors:\n", stderr); | ||
| 72 | for (const auto &error : lexer.errors) | ||
| 73 | fprintf(stderr, "- %s\n", error.c_str()); | ||
| 74 | exit(1); | ||
| 75 | } | ||
| 76 | |||
| 77 | fprintf(stderr, "Got %lu tokens\n", lexer.tokens.size()); | ||
| 78 | fprintf(stderr, "Duration: %f s\n", elapsed.count()); | ||
| 79 | fprintf(stderr, "Speed: %f MB/s\n", speed); | ||
| 80 | |||
| 81 | return std::move(lexer.tokens); | ||
| 82 | } | ||
| 83 | |||
| 84 | bool parse(const char *path, Kv1Records &into) { | ||
| 85 | std::vector<Kv1Token> tokens = lex(path); | ||
| 86 | |||
| 87 | Kv1Parser parser(tokens, into); | ||
| 88 | parser.parse(); | ||
| 89 | |||
| 90 | bool ok = true; | ||
| 91 | if (!parser.gerrors.empty()) { | ||
| 92 | ok = false; | ||
| 93 | fputs("Parser reported errors:\n", stderr); | ||
| 94 | for (const auto &error : parser.gerrors) | ||
| 95 | fprintf(stderr, "- %s\n", error.c_str()); | ||
| 96 | } | ||
| 97 | if (!parser.warns.empty()) { | ||
| 98 | fputs("Parser reported warnings:\n", stderr); | ||
| 99 | for (const auto &warn : parser.warns) | ||
| 100 | fprintf(stderr, "- %s\n", warn.c_str()); | ||
| 101 | } | ||
| 102 | |||
| 103 | fprintf(stderr, "Parsed %lu records\n", into.size()); | ||
| 104 | |||
| 105 | return ok; | ||
| 106 | } | ||
| 107 | |||
| 108 | void printParsedRecords(const Kv1Records &records) { | ||
| 109 | fputs("Parsed records:\n", stderr); | ||
| 110 | fprintf(stderr, " organizational_units: %lu\n", records.organizational_units.size()); | ||
| 111 | fprintf(stderr, " higher_organizational_units: %lu\n", records.higher_organizational_units.size()); | ||
| 112 | fprintf(stderr, " user_stop_points: %lu\n", records.user_stop_points.size()); | ||
| 113 | fprintf(stderr, " user_stop_areas: %lu\n", records.user_stop_areas.size()); | ||
| 114 | fprintf(stderr, " timing_links: %lu\n", records.timing_links.size()); | ||
| 115 | fprintf(stderr, " links: %lu\n", records.links.size()); | ||
| 116 | fprintf(stderr, " lines: %lu\n", records.lines.size()); | ||
| 117 | fprintf(stderr, " destinations: %lu\n", records.destinations.size()); | ||
| 118 | fprintf(stderr, " journey_patterns: %lu\n", records.journey_patterns.size()); | ||
| 119 | fprintf(stderr, " concession_financer_relations: %lu\n", records.concession_financer_relations.size()); | ||
| 120 | fprintf(stderr, " concession_areas: %lu\n", records.concession_areas.size()); | ||
| 121 | fprintf(stderr, " financers: %lu\n", records.financers.size()); | ||
| 122 | fprintf(stderr, " journey_pattern_timing_links: %lu\n", records.journey_pattern_timing_links.size()); | ||
| 123 | fprintf(stderr, " points: %lu\n", records.points.size()); | ||
| 124 | fprintf(stderr, " point_on_links: %lu\n", records.point_on_links.size()); | ||
| 125 | fprintf(stderr, " icons: %lu\n", records.icons.size()); | ||
| 126 | fprintf(stderr, " notices: %lu\n", records.notices.size()); | ||
| 127 | fprintf(stderr, " notice_assignments: %lu\n", records.notice_assignments.size()); | ||
| 128 | fprintf(stderr, " time_demand_groups: %lu\n", records.time_demand_groups.size()); | ||
| 129 | fprintf(stderr, " time_demand_group_run_times: %lu\n", records.time_demand_group_run_times.size()); | ||
| 130 | fprintf(stderr, " period_groups: %lu\n", records.period_groups.size()); | ||
| 131 | fprintf(stderr, " specific_days: %lu\n", records.specific_days.size()); | ||
| 132 | fprintf(stderr, " timetable_versions: %lu\n", records.timetable_versions.size()); | ||
| 133 | fprintf(stderr, " public_journeys: %lu\n", records.public_journeys.size()); | ||
| 134 | fprintf(stderr, " period_group_validities: %lu\n", records.period_group_validities.size()); | ||
| 135 | fprintf(stderr, " exceptional_operating_days: %lu\n", records.exceptional_operating_days.size()); | ||
| 136 | fprintf(stderr, " schedule_versions: %lu\n", records.schedule_versions.size()); | ||
| 137 | fprintf(stderr, " public_journey_passing_times: %lu\n", records.public_journey_passing_times.size()); | ||
| 138 | fprintf(stderr, " operating_days: %lu\n", records.operating_days.size()); | ||
| 139 | } | ||
| 140 | |||
| 141 | void printIndexSize(const Kv1Index &index) { | ||
| 142 | fputs("Index size:\n", stderr); | ||
| 143 | fprintf(stderr, " organizational_units: %lu\n", index.organizational_units.size()); | ||
| 144 | fprintf(stderr, " user_stop_points: %lu\n", index.user_stop_points.size()); | ||
| 145 | fprintf(stderr, " user_stop_areas: %lu\n", index.user_stop_areas.size()); | ||
| 146 | fprintf(stderr, " timing_links: %lu\n", index.timing_links.size()); | ||
| 147 | fprintf(stderr, " links: %lu\n", index.links.size()); | ||
| 148 | fprintf(stderr, " lines: %lu\n", index.lines.size()); | ||
| 149 | fprintf(stderr, " destinations: %lu\n", index.destinations.size()); | ||
| 150 | fprintf(stderr, " journey_patterns: %lu\n", index.journey_patterns.size()); | ||
| 151 | fprintf(stderr, " concession_financer_relations: %lu\n", index.concession_financer_relations.size()); | ||
| 152 | fprintf(stderr, " concession_areas: %lu\n", index.concession_areas.size()); | ||
| 153 | fprintf(stderr, " financers: %lu\n", index.financers.size()); | ||
| 154 | fprintf(stderr, " journey_pattern_timing_links: %lu\n", index.journey_pattern_timing_links.size()); | ||
| 155 | fprintf(stderr, " points: %lu\n", index.points.size()); | ||
| 156 | fprintf(stderr, " point_on_links: %lu\n", index.point_on_links.size()); | ||
| 157 | fprintf(stderr, " icons: %lu\n", index.icons.size()); | ||
| 158 | fprintf(stderr, " notices: %lu\n", index.notices.size()); | ||
| 159 | fprintf(stderr, " time_demand_groups: %lu\n", index.time_demand_groups.size()); | ||
| 160 | fprintf(stderr, " time_demand_group_run_times: %lu\n", index.time_demand_group_run_times.size()); | ||
| 161 | fprintf(stderr, " period_groups: %lu\n", index.period_groups.size()); | ||
| 162 | fprintf(stderr, " specific_days: %lu\n", index.specific_days.size()); | ||
| 163 | fprintf(stderr, " timetable_versions: %lu\n", index.timetable_versions.size()); | ||
| 164 | fprintf(stderr, " public_journeys: %lu\n", index.public_journeys.size()); | ||
| 165 | fprintf(stderr, " period_group_validities: %lu\n", index.period_group_validities.size()); | ||
| 166 | fprintf(stderr, " exceptional_operating_days: %lu\n", index.exceptional_operating_days.size()); | ||
| 167 | fprintf(stderr, " schedule_versions: %lu\n", index.schedule_versions.size()); | ||
| 168 | fprintf(stderr, " public_journey_passing_times: %lu\n", index.public_journey_passing_times.size()); | ||
| 169 | fprintf(stderr, " operating_days: %lu\n", index.operating_days.size()); | ||
| 170 | } | ||
| 171 | |||
| 172 | int main(int argc, char *argv[]) { | ||
| 173 | Options options = parseOptions(argc, argv); | ||
| 174 | |||
| 175 | Kv1Records records; | ||
| 176 | if (!parse(options.kv1_file_path, records)) { | ||
| 177 | fputs("Error parsing records, exiting\n", stderr); | ||
| 178 | return EXIT_FAILURE; | ||
| 179 | } | ||
| 180 | printParsedRecords(records); | ||
| 181 | fputs("Indexing...\n", stderr); | ||
| 182 | Kv1Index index(&records); | ||
| 183 | fprintf(stderr, "Indexed %lu records\n", index.size()); | ||
| 184 | // Only notice assignments are not indexed. If this equality is not valid, | ||
| 185 | // then this means that we had duplicate keys or that something else went | ||
| 186 | // wrong. That would really not be great. | ||
| 187 | assert(index.size() == records.size() - records.notice_assignments.size()); | ||
| 188 | printIndexSize(index); | ||
| 189 | fputs("Linking records...\n", stderr); | ||
| 190 | kv1LinkRecords(index); | ||
| 191 | fputs("Done linking\n", stderr); | ||
| 192 | |||
| 193 | if (options.subcommand == "joparoute"sv) jopaRoute(options, records, index); | ||
| 194 | if (options.subcommand == "journeyroute"sv) journeyRoute(options, records, index); | ||
| 195 | if (options.subcommand == "journeys"sv) journeys(options, records, index); | ||
| 196 | if (options.subcommand == "journeyinfo"sv) journeyInfo(options, records, index); | ||
| 197 | if (options.subcommand == "schedule"sv) schedule(options, records, index); | ||
| 198 | } | ||
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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #include <iostream> | ||
| 4 | #include <string_view> | ||
| 5 | #include <unordered_map> | ||
| 6 | #include <vector> | ||
| 7 | |||
| 8 | #include "daterange.hpp" | ||
| 9 | #include "schedule.hpp" | ||
| 10 | |||
| 11 | using namespace std::string_view_literals; | ||
| 12 | |||
| 13 | void schedule(const Options &options, Kv1Records &records, Kv1Index &index) { | ||
| 14 | FILE *out = stdout; | ||
| 15 | if (options.output_file_path != "-"sv) | ||
| 16 | out = fopen(options.output_file_path, "wb"); | ||
| 17 | if (!out) { | ||
| 18 | fprintf(stderr, "Open %s: %s\n", options.output_file_path, strerrordesc_np(errno)); | ||
| 19 | exit(EXIT_FAILURE); | ||
| 20 | } | ||
| 21 | |||
| 22 | std::cerr << "Generating schedule for " << options.line_planning_number << std::endl; | ||
| 23 | |||
| 24 | std::unordered_multimap<std::string, Kv1PeriodGroupValidity> period_group_validities; | ||
| 25 | for (const auto &pegr : records.period_group_validities) | ||
| 26 | period_group_validities.insert({ pegr.key.period_group_code, pegr }); | ||
| 27 | std::unordered_multimap<std::string, Kv1PublicJourney> public_journeys; | ||
| 28 | for (const auto &pujo : records.public_journeys) | ||
| 29 | public_journeys.insert({ pujo.key.timetable_version_code, pujo }); | ||
| 30 | |||
| 31 | std::cout << "line_planning_number,journey_number,date,departure_time" << std::endl; | ||
| 32 | for (const auto &tive : records.timetable_versions) { | ||
| 33 | std::vector<DateRange> tive_pegrval_ranges; | ||
| 34 | |||
| 35 | auto pegrval_range = period_group_validities.equal_range(tive.key.period_group_code); | ||
| 36 | for (auto it = pegrval_range.first; it != pegrval_range.second; it++) { | ||
| 37 | const auto &[_, pegrval] = *it; | ||
| 38 | tive_pegrval_ranges.emplace_back(pegrval.key.valid_from, pegrval.valid_thru); | ||
| 39 | } | ||
| 40 | |||
| 41 | DateRangeSeq seq(tive_pegrval_ranges.begin(), tive_pegrval_ranges.end()); | ||
| 42 | seq = seq.clampFrom(tive.valid_from); | ||
| 43 | if (tive.valid_thru) | ||
| 44 | seq = seq.clampThru(*tive.valid_thru); | ||
| 45 | |||
| 46 | for (const auto &range : seq) for (auto date : range) { | ||
| 47 | auto weekday = std::chrono::year_month_weekday(std::chrono::sys_days(date)).weekday(); | ||
| 48 | |||
| 49 | auto pujo_range = public_journeys.equal_range(tive.key.timetable_version_code); | ||
| 50 | for (auto itt = pujo_range.first; itt != pujo_range.second; itt++) { | ||
| 51 | const auto &[_, pujo] = *itt; | ||
| 52 | |||
| 53 | if (pujo.key.line_planning_number == options.line_planning_number && pujo.key.day_type.size() == 7 | ||
| 54 | && pujo.key.day_type[weekday.iso_encoding() - 1] == static_cast<char>('0' + weekday.iso_encoding())) { | ||
| 55 | std::cout << pujo.key.line_planning_number << "," << pujo.key.journey_number << "," | ||
| 56 | << date << "," << pujo.departure_time << std::endl; | ||
| 57 | } | ||
| 58 | } | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | if (options.output_file_path != "-"sv) fclose(out); | ||
| 63 | } | ||
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 @@ | |||
| 1 | // vim:set sw=2 ts=2 sts et: | ||
| 2 | |||
| 3 | #ifndef OEUF_QUERYKV1_SCHEDULE_HPP | ||
| 4 | #define OEUF_QUERYKV1_SCHEDULE_HPP | ||
| 5 | |||
| 6 | #include <tmi8/kv1_types.hpp> | ||
| 7 | #include <tmi8/kv1_index.hpp> | ||
| 8 | |||
| 9 | #include "cliopts.hpp" | ||
| 10 | |||
| 11 | void schedule(const Options &options, Kv1Records &records, Kv1Index &index); | ||
| 12 | |||
| 13 | #endif // OEUF_QUERYKV1_SCHEDULE_HPP | ||