aboutsummaryrefslogtreecommitdiffstats
path: root/src/querykv1
diff options
context:
space:
mode:
Diffstat (limited to 'src/querykv1')
-rw-r--r--src/querykv1/.envrc2
-rw-r--r--src/querykv1/.gitignore1
-rw-r--r--src/querykv1/Makefile28
-rw-r--r--src/querykv1/cliopts.cpp456
-rw-r--r--src/querykv1/cliopts.hpp35
-rw-r--r--src/querykv1/daterange.cpp91
-rw-r--r--src/querykv1/daterange.hpp118
-rw-r--r--src/querykv1/grammar.abnf44
-rw-r--r--src/querykv1/grammar.ebnf47
-rw-r--r--src/querykv1/grammar.ebnf.bak23
-rw-r--r--src/querykv1/joparoute.cpp102
-rw-r--r--src/querykv1/joparoute.hpp13
-rw-r--r--src/querykv1/journeyinfo.cpp64
-rw-r--r--src/querykv1/journeyinfo.hpp13
-rw-r--r--src/querykv1/journeyroute.cpp96
-rw-r--r--src/querykv1/journeyroute.hpp13
-rw-r--r--src/querykv1/journeys.cpp95
-rw-r--r--src/querykv1/journeys.hpp13
-rw-r--r--src/querykv1/main.cpp198
-rw-r--r--src/querykv1/schedule.cpp63
-rw-r--r--src/querykv1/schedule.hpp13
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 @@
1source_env ../../
2export 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
6CXXFLAGS=-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
12LDFLAGS=-ltmi8 -Wl,-z,defs \
13 -Wl,-z,nodlopen -Wl,-z,noexecstack \
14 -Wl,-z,relro -Wl,-z,now
15
16HDRS=cliopts.hpp daterange.hpp joparoute.hpp journeyinfo.hpp journeyroute.hpp journeys.hpp schedule.hpp
17SRCS=main.cpp cliopts.cpp daterange.cpp joparoute.cpp journeyinfo.cpp journeyroute.cpp journeys.cpp schedule.cpp
18OBJS=$(patsubst %.cpp,%.o,$(SRCS))
19
20%.o: %.cpp $(HDRS)
21 $(CXX) -c -o $@ $< $(CXXFLAGS)
22
23querykv1: $(OBJS)
24 $(CXX) -fPIE -pie -o $@ $^ $(CXXFLAGS) $(LDFLAGS)
25
26.PHONY: clean
27clean:
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
12using namespace std::string_view_literals;
13
14const char *opt_set = "";
15const char *opt_unset = nullptr;
16
17const char help[] = R"(Usage: %1$s [OPTIONS] <COMMAND>
18
19Global Options:
20 --kv1 <PATH> Path to file containing all KV1 data, '-' for stdin
21 -h, --help Print this help
22
23Commands:
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
31const char joparoute_help[] = R"(Usage: %1$s joparoute --line <NUMBER> --jopa <CODE> [OPTIONS]
32
33Options:
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
38Global Options:
39 --kv1 <PATH> Path to file containing all KV1 data, '-' for stdin
40 -h, --help Print this help
41)";
42
43const char journeyroute_help[] = R"(Usage: %1$s journeyroute --line <NUMBER> [OPTIONS]
44
45Options:
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
50Global Options:
51 --kv1 <PATH> Path to file containing all KV1 data, '-' for stdin
52 -h, --help Print this help
53)";
54
55const char journeys_help[] = R"(Usage: %1$s journeys --line <NUMBER> --begin <STOP> --end <STOP> [OPTIONS]
56
57For the --begin and --end arguments, use the following format:
58 --begin/--end stop:<USRSTOP CODE>
59 --begin/--end star:<USRSTAR CODE>
60
61Options:
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
67Global Options:
68 --kv1 <PATH> Path to file containing all KV1 data, '-' for stdin
69 -h, --help Print this help
70)";
71
72const char journeyinfo_help[] = R"(Usage: %1$s journeyinfo --line <NUMBER> --journey <NUMBER> [OPTIONS]
73
74Options:
75 --line <NUMBER> Line planning number to filter on
76 --journey <NUMBER> Journey number as in schedule
77
78Global Options:
79 --kv1 <PATH> Path to file containing all KV1 data, '-' for stdin
80 -h, --help Print this help
81)";
82
83const char schedule_help[] = R"(Usage: %1$s schedule --line <NUMBER> [OPTIONS]
84
85Options:
86 --line <NUMBER> Line planning number to generate schedule for
87 -o <PATH> Path of file to write to, '-' for stdout
88
89Global Options:
90 --kv1 <PATH> Path to file containing all KV1 data, '-' for stdin
91 -h, --help Print this help
92)";
93
94void 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
143void 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
187void 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
254void 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
296void 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
345struct ShortFlag {
346 int has_arg;
347 int c;
348};
349
350template<ShortFlag ...flags>
351const 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_),
364const std::string argarr = mkargarr<SHORT_OPTIONS LONG_OPTIONS ShortFlag(no_argument, 0)>;
365#undef X
366
367Options 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
21struct 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
30extern const char *opt_set;
31extern const char *opt_unset;
32
33Options 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
5static 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].
10DateRange::Iterator &DateRange::Iterator::operator++() {
11 ymd_ = nextDay(ymd_);
12 return *this;
13}
14
15std::chrono::year_month_day DateRange::Iterator::operator*() const {
16 return ymd_;
17}
18
19std::chrono::year_month_day DateRange::Iterator::ymd() const {
20 return ymd_;
21}
22
23DateRange::Iterator::Iterator(std::chrono::year_month_day ymd) : ymd_(ymd) {}
24
25DateRange::DateRange(std::chrono::year_month_day from, std::chrono::year_month_day thru)
26 : from_(from), thru_(thru)
27{}
28
29DateRange::Iterator DateRange::begin() const {
30 return DateRange::Iterator(from_);
31}
32
33DateRange::Iterator DateRange::end() const {
34 return DateRange::Iterator(nextDay(thru_));
35}
36
37bool DateRange::valid() const {
38 return from_ <= thru_;
39}
40
41std::chrono::year_month_day DateRange::from() const {
42 return from_;
43}
44
45std::chrono::year_month_day DateRange::thru() const {
46 return thru_;
47}
48
49bool operator==(const DateRange::Iterator a, const DateRange::Iterator b) {
50 return *a == *b;
51}
52
53DateRangeSeq::DateRangeSeq(std::initializer_list<DateRange> ranges)
54 : DateRangeSeq(ranges.begin(), ranges.end())
55{}
56
57DateRangeSeq 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
71DateRangeSeq 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
85std::vector<DateRange>::const_iterator DateRangeSeq::begin() const {
86 return ranges_.begin();
87}
88
89std::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].
14class 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
44bool operator==(const DateRange::Iterator a, const DateRange::Iterator b);
45
46template<typename Tp, typename T>
47concept DerefsTo = requires(Tp p) {
48 { *p } -> std::convertible_to<T>;
49};
50
51class 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
5document = [header NEWLINE] (comment / record / empty-line) *(NEWLINE (comment / record / empty-line)) [NEWLINE] / header
6
7header = OPENBRACK *NOTCRLF
8comment = SEMICOLON *NOTCRLF
9
10empty-line = *WHITESPACE
11
12record = field *(PIPE field)
13field = *WHITESPACE field-data *WHITESPACE
14field-data = escaped / unescaped
15
16; Unescaped fields are also allowed to contain double quotes,
17; they are just not interpreted in any special way.
18escaped = DQUOTE *(TEXTDATA / WHITESPACE / NEWLINE / PIPE / 2DQUOTE) DQUOTE
19unescaped = [TEXTDATA *(*WHITESPACE (TEXTDATA / DQUOTE))]
20
21HTAB = %x09 ; <horizontal tab, "\t">
22LF = %x0A ; <line feed, "\n">
23VTAB = %x0B ; <vertical tab, "\v">
24FF = %x0C ; <form feed, "\f">
25CR = %x0D ; <carriage return, "\r">
26SPACE = %x20 ; <space, " ">
27DQUOTE = %x22 ; "
28SEMICOLON = %x3B ; ;
29OPENBRACK = %x5B ; [
30PIPE = %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.
36TEXTDATA = %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'
40WHITESPACE = SPACE / FF / HTAB / VTAB
41
42; All codepoints excluding CR and LF
43NOTCRLF = %x00-09 / %x0B-0C / %x0E-10FFFF
44NEWLINE = 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 */
5document ::= (header NEWLINE)? (comment | record | empty-line) (NEWLINE (comment | record | empty-line))* NEWLINE? | header
6
7header ::= OPENBRACK NOTCR*
8comment ::= SEMICOLON NOTCR*
9
10empty-line ::= WHITESPACE*
11
12record ::= field (PIPE field)*
13field ::= WHITESPACE* field-data WHITESPACE*
14field-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 */
19escaped ::= (TEXTDATA | WHITESPACE | NEWLINE | PIPE | DQUOTE DQUOTE)*
20unescaped ::= (TEXTDATA (WHITESPACE* (TEXTDATA | DQUOTE))*)?
21
22HTAB ::= #x09 /* <horizontal tab, "\t"> */
23LF ::= #x0A /* <line feed, "\n"> */
24VTAB ::= #x0B /* <vertical tab, "\v"> */
25FF ::= #x0C /* <form feed, "\f"> */
26CR ::= #x0D /* <carriage return, "\r"> */
27SPACE ::= #x20 /* <space, " "> */
28DQUOTE ::= #x22 /* " */
29SEMICOLON ::= #x3B /* ; */
30OPENBRACK ::= #x5B /* [ */
31PIPE ::= #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 */
38TEXTDATA ::= [#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 */
43WHITESPACE ::= SPACE | LF | FF | HTAB | VTAB
44
45/* All codepoints excluding CR and LF */
46NOTCR ::= [#x00-#x0C#x0E-#x10FFFF]
47NEWLINE ::= 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 @@
1document ::= (header NEWLINE)? (comment | record | empty-line) (NEWLINE (comment | record | empty-line))* NEWLINE? | header
2header ::= OPENBRACK NOTCRLF*
3comment ::= SEMICOLON NOTCRLF*
4empty-line ::= WHITESPACE*
5record ::= field (PIPE field)*
6field ::= WHITESPACE* field-data WHITESPACE*
7field-data ::= escaped | unescaped
8escaped ::= DQUOTE (TEXTDATA | WHITESPACE | NEWLINE | PIPE | DQUOTE DQUOTE)* DQUOTE
9unescaped ::= (TEXTDATA (WHITESPACE* (TEXTDATA | DQUOTE))*)?
10HTAB ::= #x09
11LF ::= #x0A
12VTAB ::= #x0B
13FF ::= #x0C
14CR ::= #x0D
15SPACE ::= #x20
16DQUOTE ::= #x22
17SEMICOLON ::= #x3B
18OPENBRACK ::= #x5B
19PIPE ::= #x7C
20WHITESPACE ::= SPACE | FF | HTAB | VTAB
21NOTCRLF ::= [#x00-#x09#x0B-#x0C#x0E-#x10FFFF]
22TEXTDATA ::= [#x00-#x08#x0E-#x1F#x21#x23-#x5A#x5C-#x7B#x7D-#x10FFFF]
23NEWLINE ::= 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
9using namespace std::string_view_literals;
10
11void 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
11void 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
7void 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
11void 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
8using namespace std::string_view_literals;
9
10void 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
11void 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
10using namespace std::string_view_literals;
11
12void 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
11void 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
21using namespace std::string_view_literals;
22
23using TimingClock = std::conditional_t<
24 std::chrono::high_resolution_clock::is_steady,
25 std::chrono::high_resolution_clock,
26 std::chrono::steady_clock>;
27
28std::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
58std::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
84bool 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
108void 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
141void 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
172int 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
11using namespace std::string_view_literals;
12
13void 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
11void schedule(const Options &options, Kv1Records &records, Kv1Index &index);
12
13#endif // OEUF_QUERYKV1_SCHEDULE_HPP