aboutsummaryrefslogtreecommitdiffstats
path: root/lib/libtmi8
diff options
context:
space:
mode:
Diffstat (limited to 'lib/libtmi8')
-rw-r--r--lib/libtmi8/.envrc3
-rw-r--r--lib/libtmi8/.gitignore3
-rw-r--r--lib/libtmi8/Makefile41
-rw-r--r--lib/libtmi8/flake.lock58
-rw-r--r--lib/libtmi8/flake.nix42
-rw-r--r--lib/libtmi8/include/tmi8/kv1_index.hpp135
-rw-r--r--lib/libtmi8/include/tmi8/kv1_lexer.hpp46
-rw-r--r--lib/libtmi8/include/tmi8/kv1_parser.hpp87
-rw-r--r--lib/libtmi8/include/tmi8/kv1_types.hpp1528
-rw-r--r--lib/libtmi8/include/tmi8/kv6_parquet.hpp46
-rw-r--r--lib/libtmi8/src/kv1_index.cpp461
-rw-r--r--lib/libtmi8/src/kv1_lexer.cpp152
-rw-r--r--lib/libtmi8/src/kv1_parser.cpp1258
-rw-r--r--lib/libtmi8/src/kv1_types.cpp773
-rw-r--r--lib/libtmi8/src/kv6_parquet.cpp102
15 files changed, 4735 insertions, 0 deletions
diff --git a/lib/libtmi8/.envrc b/lib/libtmi8/.envrc
new file mode 100644
index 0000000..4e0d702
--- /dev/null
+++ b/lib/libtmi8/.envrc
@@ -0,0 +1,3 @@
1use flake
2
3export DEVMODE=1
diff --git a/lib/libtmi8/.gitignore b/lib/libtmi8/.gitignore
new file mode 100644
index 0000000..f6b8cf6
--- /dev/null
+++ b/lib/libtmi8/.gitignore
@@ -0,0 +1,3 @@
1src/*.o
2libtmi8.a
3libtmi8.so
diff --git a/lib/libtmi8/Makefile b/lib/libtmi8/Makefile
new file mode 100644
index 0000000..52a9807
--- /dev/null
+++ b/lib/libtmi8/Makefile
@@ -0,0 +1,41 @@
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 -Iinclude $(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=-larrow -lparquet -Wl,-z,defs \
13 -Wl,-z,nodlopen -Wl,-z,noexecstack \
14 -Wl,-z,relro -Wl,-z,now
15DESTDIR=/usr/local
16
17LIBHDRS=include/tmi8/kv1_lexer.hpp include/tmi8/kv1_parser.hpp include/tmi8/kv1_types.hpp include/tmi8/kv6_parquet.hpp
18LIBSRCS=src/kv1_index.cpp src/kv1_lexer.cpp src/kv1_parser.cpp src/kv1_types.cpp src/kv6_parquet.cpp
19LIBOBJS=$(patsubst %.cpp,%.o,$(LIBSRCS))
20
21.PHONY: all install libtmi8 clean
22all: libtmi8
23
24libtmi8: libtmi8.a libtmi8.so
25
26clean:
27 rm libtmi8.a libtmi8.so $(LIBOBJS)
28
29install: libtmi8.a $(LIBHDRS)
30 install -D -m644 include/tmi8/* -t $(DESTDIR)/include/tmi8
31 install -D -m644 libtmi8.a -t $(DESTDIR)/lib
32 install -D -m644 libtmi8.so -t $(DESTDIR)/lib
33
34src/%.o: src/%.cpp $(LIBHDRS)
35 $(CXX) -c -o $@ $< $(CXXFLAGS)
36
37libtmi8.a: $(LIBOBJS)
38 $(AR) rcs $@ $^
39
40libtmi8.so: $(LIBOBJS)
41 $(CXX) -shared -fPIC -o $@ $^ $(CXXFLAGS) $(LDFLAGS)
diff --git a/lib/libtmi8/flake.lock b/lib/libtmi8/flake.lock
new file mode 100644
index 0000000..5ff7d5d
--- /dev/null
+++ b/lib/libtmi8/flake.lock
@@ -0,0 +1,58 @@
1{
2 "nodes": {
3 "flake-utils": {
4 "inputs": {
5 "systems": "systems"
6 },
7 "locked": {
8 "lastModified": 1701680307,
9 "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
10 "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
11 "revCount": 88,
12 "type": "tarball",
13 "url": "https://api.flakehub.com/f/pinned/numtide/flake-utils/0.1.88+rev-4022d587cbbfd70fe950c1e2083a02621806a725/018c340d-3287-7c66-818b-f2f646a808e3/source.tar.gz"
14 },
15 "original": {
16 "type": "tarball",
17 "url": "https://flakehub.com/f/numtide/flake-utils/0.1.88.tar.gz"
18 }
19 },
20 "nixpkgs": {
21 "locked": {
22 "lastModified": 1701539137,
23 "narHash": "sha256-nVO/5QYpf1GwjvtpXhyxx5M3U/WN0MwBro4Lsk+9mL0=",
24 "rev": "933d7dc155096e7575d207be6fb7792bc9f34f6d",
25 "revCount": 552571,
26 "type": "tarball",
27 "url": "https://api.flakehub.com/f/pinned/NixOs/nixpkgs/0.2311.552571+rev-933d7dc155096e7575d207be6fb7792bc9f34f6d/018c3242-a93c-7779-8d13-ddba0a38d24a/source.tar.gz"
28 },
29 "original": {
30 "type": "tarball",
31 "url": "https://flakehub.com/f/NixOs/nixpkgs/*.tar.gz"
32 }
33 },
34 "root": {
35 "inputs": {
36 "flake-utils": "flake-utils",
37 "nixpkgs": "nixpkgs"
38 }
39 },
40 "systems": {
41 "locked": {
42 "lastModified": 1681028828,
43 "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
44 "owner": "nix-systems",
45 "repo": "default",
46 "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
47 "type": "github"
48 },
49 "original": {
50 "owner": "nix-systems",
51 "repo": "default",
52 "type": "github"
53 }
54 }
55 },
56 "root": "root",
57 "version": 7
58}
diff --git a/lib/libtmi8/flake.nix b/lib/libtmi8/flake.nix
new file mode 100644
index 0000000..2ae7fc9
--- /dev/null
+++ b/lib/libtmi8/flake.nix
@@ -0,0 +1,42 @@
1{
2 inputs = {
3 nixpkgs.url = "https://flakehub.com/f/NixOs/nixpkgs/*.tar.gz";
4 flake-utils.url = "https://flakehub.com/f/numtide/flake-utils/0.1.88.tar.gz";
5 };
6
7 outputs = { self, nixpkgs, flake-utils, ... }@inputs:
8 flake-utils.lib.eachDefaultSystem
9 (system:
10 let
11 pkgs = import nixpkgs {
12 inherit system;
13 overlays = [ ];
14 };
15
16 inherit (pkgs.gcc13) stdenv;
17
18 oeuf-libtmi8 = stdenv.mkDerivation {
19 name = "oeuf-libtmi8";
20 src = pkgs.lib.cleanSource ./.;
21
22 nativeBuildInputs = with pkgs; [ gcc13 ];
23 buildInputs = with pkgs; [ arrow-cpp boost182 ];
24 buildPhase = ''
25 make libtmi8
26 '';
27
28 installPhase = ''
29 make install DESTDIR="$out"
30 '';
31 };
32 in
33 {
34 packages.oeuf-libtmi8 = oeuf-libtmi8;
35
36 devShells.default = pkgs.mkShell {
37 inputsFrom = [ oeuf-libtmi8 ];
38 };
39
40 formatter = pkgs.nixpkgs-fmt;
41 });
42}
diff --git a/lib/libtmi8/include/tmi8/kv1_index.hpp b/lib/libtmi8/include/tmi8/kv1_index.hpp
new file mode 100644
index 0000000..621acf6
--- /dev/null
+++ b/lib/libtmi8/include/tmi8/kv1_index.hpp
@@ -0,0 +1,135 @@
1// vim:set sw=2 ts=2 sts et:
2
3#ifndef OEUF_LIBTMI8_KV1_INDEX_HPP
4#define OEUF_LIBTMI8_KV1_INDEX_HPP
5
6#include <unordered_map>
7
8#include <boost/container_hash/hash.hpp>
9
10#include <tmi8/kv1_types.hpp>
11
12struct Kv1Index {
13 Kv1Records *records;
14
15 explicit Kv1Index(Kv1Records *records);
16
17 std::unordered_map<
18 Kv1OrganizationalUnit::Key,
19 Kv1OrganizationalUnit *,
20 boost::hash<Kv1OrganizationalUnit::Key>> organizational_units;
21 std::unordered_map<
22 Kv1HigherOrganizationalUnit::Key,
23 Kv1HigherOrganizationalUnit *,
24 boost::hash<Kv1HigherOrganizationalUnit::Key>> higher_organizational_units;
25 std::unordered_map<
26 Kv1UserStopPoint::Key,
27 Kv1UserStopPoint *,
28 boost::hash<Kv1UserStopPoint::Key>> user_stop_points;
29 std::unordered_map<
30 Kv1UserStopArea::Key,
31 Kv1UserStopArea *,
32 boost::hash<Kv1UserStopArea::Key>> user_stop_areas;
33 std::unordered_map<
34 Kv1TimingLink::Key,
35 Kv1TimingLink *,
36 boost::hash<Kv1TimingLink::Key>> timing_links;
37 std::unordered_map<
38 Kv1Link::Key,
39 Kv1Link *,
40 boost::hash<Kv1Link::Key>> links;
41 std::unordered_map<
42 Kv1Line::Key,
43 Kv1Line *,
44 boost::hash<Kv1Line::Key>> lines;
45 std::unordered_map<
46 Kv1Destination::Key,
47 Kv1Destination *,
48 boost::hash<Kv1Destination::Key>> destinations;
49 std::unordered_map<
50 Kv1JourneyPattern::Key,
51 Kv1JourneyPattern *,
52 boost::hash<Kv1JourneyPattern::Key>> journey_patterns;
53 std::unordered_map<
54 Kv1ConcessionFinancerRelation::Key,
55 Kv1ConcessionFinancerRelation *,
56 boost::hash<Kv1ConcessionFinancerRelation::Key>> concession_financer_relations;
57 std::unordered_map<
58 Kv1ConcessionArea::Key,
59 Kv1ConcessionArea *,
60 boost::hash<Kv1ConcessionArea::Key>> concession_areas;
61 std::unordered_map<
62 Kv1Financer::Key,
63 Kv1Financer *,
64 boost::hash<Kv1Financer::Key>> financers;
65 std::unordered_map<
66 Kv1JourneyPatternTimingLink::Key,
67 Kv1JourneyPatternTimingLink *,
68 boost::hash<Kv1JourneyPatternTimingLink::Key>> journey_pattern_timing_links;
69 std::unordered_map<
70 Kv1Point::Key,
71 Kv1Point *,
72 boost::hash<Kv1Point::Key>> points;
73 std::unordered_map<
74 Kv1PointOnLink::Key,
75 Kv1PointOnLink *,
76 boost::hash<Kv1PointOnLink::Key>> point_on_links;
77 std::unordered_map<
78 Kv1Icon::Key,
79 Kv1Icon *,
80 boost::hash<Kv1Icon::Key>> icons;
81 std::unordered_map<
82 Kv1Notice::Key,
83 Kv1Notice *,
84 boost::hash<Kv1Notice::Key>> notices;
85 std::unordered_map<
86 Kv1TimeDemandGroup::Key,
87 Kv1TimeDemandGroup *,
88 boost::hash<Kv1TimeDemandGroup::Key>> time_demand_groups;
89 std::unordered_map<
90 Kv1TimeDemandGroupRunTime::Key,
91 Kv1TimeDemandGroupRunTime *,
92 boost::hash<Kv1TimeDemandGroupRunTime::Key>> time_demand_group_run_times;
93 std::unordered_map<
94 Kv1PeriodGroup::Key,
95 Kv1PeriodGroup *,
96 boost::hash<Kv1PeriodGroup::Key>> period_groups;
97 std::unordered_map<
98 Kv1SpecificDay::Key,
99 Kv1SpecificDay *,
100 boost::hash<Kv1SpecificDay::Key>> specific_days;
101 std::unordered_map<
102 Kv1TimetableVersion::Key,
103 Kv1TimetableVersion *,
104 boost::hash<Kv1TimetableVersion::Key>> timetable_versions;
105 std::unordered_map<
106 Kv1PublicJourney::Key,
107 Kv1PublicJourney *,
108 boost::hash<Kv1PublicJourney::Key>> public_journeys;
109 std::unordered_map<
110 Kv1PeriodGroupValidity::Key,
111 Kv1PeriodGroupValidity *,
112 boost::hash<Kv1PeriodGroupValidity::Key>> period_group_validities;
113 std::unordered_map<
114 Kv1ExceptionalOperatingDay::Key,
115 Kv1ExceptionalOperatingDay *,
116 boost::hash<Kv1ExceptionalOperatingDay::Key>> exceptional_operating_days;
117 std::unordered_map<
118 Kv1ScheduleVersion::Key,
119 Kv1ScheduleVersion *,
120 boost::hash<Kv1ScheduleVersion::Key>> schedule_versions;
121 std::unordered_map<
122 Kv1PublicJourneyPassingTimes::Key,
123 Kv1PublicJourneyPassingTimes *,
124 boost::hash<Kv1PublicJourneyPassingTimes::Key>> public_journey_passing_times;
125 std::unordered_map<
126 Kv1OperatingDay::Key,
127 Kv1OperatingDay *,
128 boost::hash<Kv1OperatingDay::Key>> operating_days;
129
130 size_t size() const;
131};
132
133void kv1LinkRecords(Kv1Index &index);
134
135#endif // OEUF_LIBTMI8_KV1_INDEX_HPP
diff --git a/lib/libtmi8/include/tmi8/kv1_lexer.hpp b/lib/libtmi8/include/tmi8/kv1_lexer.hpp
new file mode 100644
index 0000000..df6a57c
--- /dev/null
+++ b/lib/libtmi8/include/tmi8/kv1_lexer.hpp
@@ -0,0 +1,46 @@
1// vim:set sw=2 ts=2 sts et:
2
3#ifndef OEUF_LIBTMI8_KV1_LEXER_HPP
4#define OEUF_LIBTMI8_KV1_LEXER_HPP
5
6#include <cstdint>
7#include <cstring>
8#include <iostream>
9#include <string>
10#include <vector>
11#include <variant>
12
13enum Kv1TokenType {
14 KV1_TOKEN_CELL,
15 KV1_TOKEN_ROW_END,
16};
17struct Kv1Token { Kv1TokenType type; std::string data; };
18
19struct Kv1Lexer {
20 std::vector<std::string> errors;
21 std::vector<Kv1Token> tokens;
22
23 explicit Kv1Lexer(std::string_view input);
24
25 void lex();
26
27 private:
28 // Does not eat newline character.
29 void eatRestOfLine();
30 void lexOptionalHeader();
31 void lexOptionalComment();
32
33 static bool isWhitespace(int c);
34
35 void readQuotedColumn();
36 void readUnquotedColumn();
37 void lexRow();
38 // Returns true when a line ending was consumed.
39 bool eatWhitespace();
40
41 std::string_view input;
42 std::string_view slice;
43 std::string colbuf;
44};
45
46#endif // OEUF_LIBTMI8_KV1_LEXER_HPP
diff --git a/lib/libtmi8/include/tmi8/kv1_parser.hpp b/lib/libtmi8/include/tmi8/kv1_parser.hpp
new file mode 100644
index 0000000..ccd8ec6
--- /dev/null
+++ b/lib/libtmi8/include/tmi8/kv1_parser.hpp
@@ -0,0 +1,87 @@
1// vim:set sw=2 ts=2 sts et:
2
3#ifndef OEUF_LIBTMI8_KV1_PARSER_HPP
4#define OEUF_LIBTMI8_KV1_PARSER_HPP
5
6#include <optional>
7#include <string>
8#include <string_view>
9#include <unordered_map>
10#include <vector>
11
12#include <tmi8/kv1_lexer.hpp>
13#include <tmi8/kv1_types.hpp>
14
15struct Kv1Parser {
16 explicit Kv1Parser(std::vector<Kv1Token> tokens, Kv1Records &parse_into);
17
18 void parse();
19
20 private:
21 // Method pointer to a method of Kv1Parser (i.e. a function that takes
22 // 'this'; is not static) that takes no arguments and also does not return
23 // anything.
24 using ParseFunc = void (Kv1Parser::*)();
25 static const std::unordered_map<std::string_view, ParseFunc> type_parsers;
26
27 bool atEnd() const;
28 void eatRowEnds();
29 const Kv1Token *cur() const;
30 const std::string *eatCell(std::string_view parsing_what);
31 std::string parseHeader();
32 void eatRestOfRow();
33
34 void requireString(std::string_view field, bool mandatory, size_t max_length, std::string_view value);
35 std::optional<bool> requireBoolean(std::string_view field, bool mandatory, std::string_view value);
36 std::optional<double> requireNumber(std::string_view field, bool mandatory, size_t max_digits, std::string_view value);
37 std::optional<RgbColor> requireRgbColor(std::string_view field, bool mandatory, std::string_view value);
38 std::optional<double> requireRdCoord(std::string_view field, bool mandatory, size_t min_digits, std::string_view value);
39
40 std::string eatString(std::string_view field, bool mandatory, size_t max_length);
41 std::optional<bool> eatBoolean(std::string_view field, bool mandatory);
42 std::optional<double> eatNumber(std::string_view field, bool mandatory, size_t max_digits);
43 std::optional<RgbColor> eatRgbColor(std::string_view field, bool mandatory);
44 std::optional<double> eatRdCoord(std::string_view field, bool mandatory, size_t min_digits);
45
46 void parseOrganizationalUnit();
47 void parseHigherOrganizationalUnit();
48 void parseUserStopPoint();
49 void parseUserStopArea();
50 void parseTimingLink();
51 void parseLink();
52 void parseLine();
53 void parseDestination();
54 void parseJourneyPattern();
55 void parseConcessionFinancerRelation();
56 void parseConcessionArea();
57 void parseFinancer();
58 void parseJourneyPatternTimingLink();
59 void parsePoint();
60 void parsePointOnLink();
61 void parseIcon();
62 void parseNotice();
63 void parseNoticeAssignment();
64 void parseTimeDemandGroup();
65 void parseTimeDemandGroupRunTime();
66 void parsePeriodGroup();
67 void parseSpecificDay();
68 void parseTimetableVersion();
69 void parsePublicJourney();
70 void parsePeriodGroupValidity();
71 void parseExceptionalOperatingDay();
72 void parseScheduleVersion();
73 void parsePublicJourneyPassingTimes();
74 void parseOperatingDay();
75
76 size_t pos = 0;
77 std::vector<Kv1Token> tokens;
78 const std::chrono::time_zone *amsterdam = std::chrono::locate_zone("Europe/Amsterdam");
79
80 public:
81 std::vector<std::string> warns;
82 std::vector<std::string> global_errors;
83 std::vector<std::string> record_errors;
84 Kv1Records &records;
85};
86
87#endif // OEUF_LIBTMI8_KV1_PARSER_HPP
diff --git a/lib/libtmi8/include/tmi8/kv1_types.hpp b/lib/libtmi8/include/tmi8/kv1_types.hpp
new file mode 100644
index 0000000..d4a0760
--- /dev/null
+++ b/lib/libtmi8/include/tmi8/kv1_types.hpp
@@ -0,0 +1,1528 @@
1// vim:set sw=2 ts=2 sts et:
2
3#ifndef OEUF_LIBTMI8_KV1_TYPES_HPP
4#define OEUF_LIBTMI8_KV1_TYPES_HPP
5
6#include <chrono>
7#include <cstdint>
8#include <optional>
9#include <string>
10#include <variant>
11
12struct Kv1OrganizationalUnit;
13struct Kv1HigherOrganizationalUnit;
14struct Kv1UserStopPoint;
15struct Kv1UserStopArea;
16struct Kv1TimingLink;
17struct Kv1Link;
18struct Kv1Line;
19struct Kv1Destination;
20struct Kv1JourneyPattern;
21struct Kv1ConcessionFinancerRelation;
22struct Kv1ConcessionArea;
23struct Kv1Financer;
24struct Kv1JourneyPatternTimingLink;
25struct Kv1Point;
26struct Kv1PointOnLink;
27struct Kv1Icon;
28struct Kv1Notice;
29struct Kv1NoticeAssignment;
30struct Kv1TimeDemandGroup;
31struct Kv1TimeDemandGroupRunTime;
32struct Kv1PeriodGroup;
33struct Kv1SpecificDay;
34struct Kv1TimetableVersion;
35struct Kv1PublicJourney;
36struct Kv1PeriodGroupValidity;
37struct Kv1ExceptionalOperatingDay;
38struct Kv1ScheduleVersion;
39struct Kv1PublicJourneyPassingTimes;
40struct Kv1OperatingDay;
41
42struct Kv1Records {
43 std::vector<Kv1OrganizationalUnit> organizational_units;
44 std::vector<Kv1HigherOrganizationalUnit> higher_organizational_units;
45 std::vector<Kv1UserStopPoint> user_stop_points;
46 std::vector<Kv1UserStopArea> user_stop_areas;
47 std::vector<Kv1TimingLink> timing_links;
48 std::vector<Kv1Link> links;
49 std::vector<Kv1Line> lines;
50 std::vector<Kv1Destination> destinations;
51 std::vector<Kv1JourneyPattern> journey_patterns;
52 std::vector<Kv1ConcessionFinancerRelation> concession_financer_relations;
53 std::vector<Kv1ConcessionArea> concession_areas;
54 std::vector<Kv1Financer> financers;
55 std::vector<Kv1JourneyPatternTimingLink> journey_pattern_timing_links;
56 std::vector<Kv1Point> points;
57 std::vector<Kv1PointOnLink> point_on_links;
58 std::vector<Kv1Icon> icons;
59 std::vector<Kv1Notice> notices;
60 std::vector<Kv1NoticeAssignment> notice_assignments;
61 std::vector<Kv1TimeDemandGroup> time_demand_groups;
62 std::vector<Kv1TimeDemandGroupRunTime> time_demand_group_run_times;
63 std::vector<Kv1PeriodGroup> period_groups;
64 std::vector<Kv1SpecificDay> specific_days;
65 std::vector<Kv1TimetableVersion> timetable_versions;
66 std::vector<Kv1PublicJourney> public_journeys;
67 std::vector<Kv1PeriodGroupValidity> period_group_validities;
68 std::vector<Kv1ExceptionalOperatingDay> exceptional_operating_days;
69 std::vector<Kv1ScheduleVersion> schedule_versions;
70 std::vector<Kv1PublicJourneyPassingTimes> public_journey_passing_times;
71 std::vector<Kv1OperatingDay> operating_days;
72
73 size_t size() const;
74};
75
76// These definitions implement TMI8, KV1 Dienstregeling (Timetable) version
77// 8.3.0.2 (release), published by BISON on January 8, 2020.
78// (Filename: tmi8 dienstregeling (kv 1) v8.3.0.2, release.docx)
79//
80// This specification and other BISON specifications, as well as other
81// supplementary information, can be found on BISON's website:
82// https://bison.dova.nu/
83//
84// The specification that was used to create these definitions was downloaded
85// from the following address:
86// https://bison.dova.nu/sites/default/files/bestanden/tmi8_dienstregeling_kv_1_v8.3.0.2_release.pdf
87//
88// The KV1 table structure and the corresponding documentation describing the
89// relevant tables and fields, as presented here, is derived from the original
90// specification. Most documentation is a manually translated version of the
91// documentation as present in the specification. The specification is licensed
92// under CC BY-ND 3.0. The exact text of this license can be found on
93// https://creativecommons.org/licenses/by-nd/3.0/nl/.
94
95// KV1 Table 1: Organizational Unit [ORUN] (MANDATORY)
96//
97// A collection of trips with the same validity features. An organizational
98// unit can be part of a 'higher' unit.
99//
100// An organizational unit is defined as a unity vor which the planning of trips
101// is compiled. When defining the organizational units, it is important that
102// all trips within the package have a homogeneous validity (school holidays,
103// shopping Sundays, foreign bank holidays).
104//
105// This table is part of the core data tables, which are common for all KV1
106// variants.
107struct Kv1OrganizationalUnit {
108 struct Key {
109 // Mandatory (key), at most 10 characters. Transport operator (from list as
110 // defined in BISON enumeration E1).
111 std::string data_owner_code;
112 // Mandatory (key), at most 10 characters.
113 std::string organizational_unit_code;
114
115 explicit Key(std::string data_owner_code,
116 std::string organizational_unit_code);
117 };
118
119 Key key;
120 // Mandatory, at most 50 characters.
121 std::string name;
122 // Mandatory, at most 10 characters.
123 std::string organizational_unit_type;
124 // Optional, at most 255 characters.
125 std::string description;
126};
127
128// KV1 Table 2: Higher Organizational Unit [ORUNORUN] (OPTIONAL)
129//
130// An in the hierarchy higher-ordered organizational unit for the purpose of
131// (among others) recording of (deviating) validities on the high level.
132//
133// This table is part of the core data tables, which are common for all KV1
134// variants.
135struct Kv1HigherOrganizationalUnit {
136 struct Key {
137 // Mandatory (key), at most 10 characters. Transport operator (from list as
138 // defined in BISON enumeration E1).
139 std::string data_owner_code;
140 // Mandatory (key), at most 10 characters. Parent, higher organizational unit
141 // that is referred to.
142 std::string organizational_unit_code_parent;
143 // Mandatory (key), at most 10 characters. Child, lower organizational unit.
144 std::string organizational_unit_code_child;
145 // Mandatory (key), at most 10 characters. [YYYY-MM-DD] Starting date of the
146 // hierarchical relation (can be a fixed value, e.g. 2006-12-31).
147 std::chrono::year_month_day valid_from;
148
149 explicit Key(std::string data_owner_code,
150 std::string organizational_unit_code_parent,
151 std::string organizational_unit_code_child,
152 std::chrono::year_month_day valid_from);
153 };
154
155 Key key;
156
157 Kv1OrganizationalUnit *p_organizational_unit_parent = nullptr;
158 Kv1OrganizationalUnit *p_organizational_unit_child = nullptr;
159};
160
161// KV1 Table 3: User Stop Point [USRSTOP]
162//
163// Stop or other point (e.g. Bridge, functioning as info for the bridge keeper)
164// for which times are recorded in the planning system of the transit operator.
165//
166// Coordinates of a UserStopPoint are recorded as Point. When defining
167// UserStopPoints, it is important that the coordinates can be unambiguously
168// and verifiably recorded. For a stop, the coordinates of the stop sign are
169// recorded. If there is no stop sign, the end of the bus stop (where the bus
170// normally halts) is recorded as the coordinate of the stop.
171//
172// This table is part of the core data tables, which are common for all KV1
173// variants.
174struct Kv1UserStopPoint {
175 struct Key {
176 // Mandatory (key), at most 10 characters. Transport operator (from list as
177 // defined in BISON enumeration E1).
178 std::string data_owner_code;
179 // Mandatory (key), at most 10 characters. Stop number in domain of operator.
180 std::string user_stop_code;
181
182 explicit Key(std::string data_owner_code,
183 std::string user_stop_code);
184 };
185
186 Key key;
187 // Optional, at most 10 characters. Stop number in domain of integrator,
188 // (initially) equal to UserStopCode.
189 std::string timing_point_code;
190 // Mandatory, at most 5 characters. Boolean indicator whether USRSTOP is used
191 // as boarding stop, true by default. False for e.g. dummy stop for bridge
192 // keeper.
193 bool get_in = true;
194 // Mandatory, at most 5 characters. Boolean indicator whether USRSTOP is used
195 // as alighting stop.
196 bool get_out = false;
197 // Mandatory, at most 50 characters. Stop name.
198 std::string name;
199 // Mandatory, at most 50 characters. Town name.
200 std::string town;
201 // Optional, at most 10 characters. Reference to StopArea of which the
202 // UserStop is part.
203 std::string user_stop_area_code;
204 // Mandatory, at most 10 characters. Platform indication/letter. The '-'
205 // value is used to indication that this is not applicable.
206 std::string stop_side_code;
207 // Mandatory, at most 5 digits. Minimal stop duration for boarding and
208 // alighting, zero by default. In seconds.
209 double minimal_stop_time_s = 0;
210 // Optional, at most 3 digits. Length of stop platform.
211 std::optional<double> stop_side_length;
212 // Optional, at most 255 characters.
213 std::string description;
214 // Mandatory, at most 10 characters. USRSTOPTYPE. Indicates the stop kind.
215 std::string user_stop_type;
216 // Optional, at most 30 characters. Nationally unique stop number.
217 std::string quay_code;
218
219 Kv1UserStopArea *p_user_stop_area = nullptr;
220 Kv1Point *p_point = nullptr;
221};
222
223// KV1 Table 4: User Stop Area [USRSTAR]
224//
225// A StopArea is a collection of stops, which have the same name for passengers
226// and logically belong together. (E.g. a bus station of transfer point.) Stops
227// lying opposite each other can also form a StopArea.
228//
229// Used for display of all stops in a stop area on an overview display and for
230// announcement of stop names (stops on both sides of the street share the same
231// name).
232//
233// This table is part of the core data tables, which are common for all KV1
234// variants.
235struct Kv1UserStopArea {
236 struct Key {
237 // Mandatory (key), at most 10 characters. Transport operator (from list as
238 // defined in BISON enumeration E1).
239 std::string data_owner_code;
240 // Mandatory (key), at most 10 characters. Code of StopArea following coding
241 // of operator, e.g. PlaceCode.
242 std::string user_stop_area_code;
243
244 explicit Key(std::string data_owner_code,
245 std::string user_stop_area_code);
246 };
247
248 Key key;
249 // Mandatory, at most 50 characters.
250 std::string name;
251 // Mandatory, at most 50 characters.
252 std::string town;
253 // Mandatory, at most 255 characters.
254 std::string description;
255};
256
257// KV1 Table 5: Timing Link [TILI]
258//
259// Link between two points which have the feature 'stop' or 'timing point'. A
260// Timing Link is set between all stops and other timing points (e.g. for the
261// bridge) which make part of a journey pattern.
262//
263// This table is part of the core data tables, which are common for all KV1
264// variants.
265struct Kv1TimingLink {
266 struct Key {
267 // Mandatory (key), at most 10 characters. Transport operator (from list as
268 // defined in BISON enumeration E1).
269 std::string data_owner_code;
270 // Mandatory (key), at most 10 characters. Stop number in the domain of
271 // DataOwner (here: the operator).
272 std::string user_stop_code_begin;
273 // Mandatory (key), at most 10 characters. Stop number in the domain of
274 // DataOwner (here: the operator).
275 std::string user_stop_code_end;
276
277 explicit Key(std::string data_owner_code,
278 std::string user_stop_code_begin,
279 std::string user_stop_code_end);
280 };
281
282 Key key;
283 // Optional, at most 5 digits. Minimal trip time (in seconds).
284 std::optional<double> minimal_drive_time_s;
285 // Optional, at most 255 characters.
286 std::string description;
287
288 Kv1UserStopPoint *p_user_stop_begin = nullptr;
289 Kv1UserStopPoint *p_user_stop_end = nullptr;
290};
291
292// KV1 Table 6: Link [LINK]
293//
294// A route link describes the connection between to points on the physical path
295// of a route.
296//
297// This table is part of the core data tables, which are common for all KV1
298// variants.
299struct Kv1Link {
300 struct Key {
301 // Mandatory (key), at most 10 characters. Transport operator (from list as
302 // defined in BISON enumeration E1).
303 std::string data_owner_code;
304 // Mandatory (key), at most 10 characters. Stop code in the domain of
305 // DataOwner (here: the operator).
306 std::string user_stop_code_begin;
307 // Mandatory (key), at most 10 characters. Stop code in the domain of
308 // DataOwner (here: the operator).
309 std::string user_stop_code_end;
310 // Mandatory (key), at most 5 characters. Modality for which the distance
311 // applies, see BISON enumeration E9.
312 // TODO: Check if BISON enumeration E9 can be put into an enum.
313 std::string transport_type;
314
315 explicit Key(std::string data_owner_code,
316 std::string user_stop_code_begin,
317 std::string user_stop_code_end,
318 std::string transport_type);
319 };
320
321 Key key;
322 // Mandatory, at most 6 digits. Length of the link (in meters).
323 double distance = 0;
324 // Optional, at most 255 characters.
325 std::string description;
326
327 Kv1UserStopPoint *p_user_stop_begin = nullptr;
328 Kv1UserStopPoint *p_user_stop_end = nullptr;
329};
330
331struct RgbColor {
332 uint8_t r, g, b = 0;
333};
334
335// KV1 Table 7: Line [LINE]
336//
337// A line is a collection of routes/journey patterns which is publically known
338// under a shared number.
339//
340// This table is part of the core data tables, which are common for all KV1
341// variants.
342struct Kv1Line {
343 struct Key {
344 // Mandatory (key), at most 10 characters. Transport operator (from list as
345 // defined in BISON enumeration E1).
346 std::string data_owner_code;
347 // Mandatory (key), at most 10 characters. Unique system line number in the
348 // domain of DataOwner.
349 std::string line_planning_number;
350
351 explicit Key(std::string data_owner_code,
352 std::string line_planning_number);
353 };
354
355 Key key;
356 // Mandatory, at most 4 characters. Line number for the public, incl. S/N
357 // indications.
358 std::string line_public_number;
359 // Mandatory, at most 50 characters.
360 std::string line_name;
361 // Mandatory, at most three digits. Should be in the range [0, 400).
362 // Only processing Connexxion's KV1 export, however, shows us that this range
363 // constrained is not honored in practice. That is why we also don't care.
364 short line_ve_tag_number = 0;
365 // Optional, at most 255 characters.
366 std::string description;
367 // Mandatory, at most 5 characters. Modality, see BISON enumeration E9.
368 // TODO: Check if BISON enumeration E9 can be put into an enum.
369 std::string transport_type;
370 // Optional, at most 4 digits. Symbol / image for the line. Reference to ICON
371 // table.
372 std::optional<short> line_icon;
373 // Optional, at most four characters. Background color for the line.
374 // Hexadecimal representation following RGB coding. Always six characters
375 // (RRGGBB), only numbers and/or capital letters.
376 std::optional<RgbColor> line_color;
377 // Optional, at most four characters. Foreground color for the line.
378 // Hexadecimal representation following RGB coding. Always six characters
379 // (RRGGBB), only numbers and/or capital letters.
380 std::optional<RgbColor> line_text_color;
381
382 Kv1Icon *p_line_icon = nullptr;
383};
384
385// KV1 Table 8: Destination [DEST]
386//
387// A destination shows the place/district/description of the route for the
388// passenger. Intermediate and detail destinations of a journey pattern are
389// shown under a single desination code, together with the primary destination.
390//
391// This table is part of the core data tables, which are common for all KV1
392// variants.
393struct Kv1Destination {
394 struct Key {
395 // Mandatory (key), at most 10 characters. Transport operator (from list as
396 // defined in BISON enumeration E1).
397 std::string data_owner_code;
398 // Mandatory (key), at most 10 characters.
399 std::string dest_code;
400
401 explicit Key(std::string data_owner_code,
402 std::string dest_code);
403 };
404
405 Key key;
406 // Mandatory, at most 50 characters. Full destination (e.g. compiled from
407 // primary, detail or intermediate destination).
408 std::string dest_name_full;
409 // Mandatory, at most 24 characters. Primary / intermediate destination in
410 // enumeration / final destination if 1 line is used.
411 std::string dest_name_main;
412 // Optional, at most 24 characters. Detail/secondary or intermediate
413 // destination for primary desination, final destination (for intermediate
414 // destination on line 1).
415 std::string dest_name_detail;
416 // Mandatory, at most 5 characters. Boolean which indcates whether
417 // DestNameDetail must always be shown (e.g. because this contains an
418 // important intermediate destination.)
419 bool relevant_dest_name_detail = false;
420 // Mandatory, at most 21 characters. Primary destination in 21 characters.
421 std::string dest_name_main_21;
422 // Optional, at most 21 characters. Detail/secondary/intermediate destination
423 // in 21 characters.
424 std::string dest_name_detail_21;
425 // Mandatory, at most 19 characters. Primary destination in 19 characters.
426 std::string dest_name_main_19;
427 // Optional, at most 19 characters. Detail/secondary/intermediate destination
428 // in 19 characters.
429 std::string dest_name_detail_19;
430 // Mandatory, at most 16 characters. Primary destination in 16 characters.
431 std::string dest_name_main_16;
432 // Optional, at most 16 characters. Detail/secondary/intermediate destination
433 // in 16 characters.
434 std::string dest_name_detail_16;
435 // Optional, at most 4 digits. Symbol/image for the destination. Reference to
436 // the ICON table.
437 std::optional<short> dest_icon;
438 // Optional, at most 6 characters. Background color for the destination.
439 // Hexadecimal representation following RGB coding. Always six characters
440 // (RRGGBB), only six digits and/or capital letters.
441 std::optional<RgbColor> dest_color;
442 // Optional, at most 30 characters (WTF?). Foreground color for the
443 // destination. Hexadecimal representation following RGB coding. Always six
444 // characters (RRGGBB), only six digits and/or capital letters.
445 std::optional<RgbColor> dest_text_color;
446};
447
448// KV1 Table 9: Journey Pattern [JOPA]
449//
450// The journey pattern describes the route from start to end point as a ordered
451// list of stops and links between stops/timing points.
452//
453// This table is part of the core data tables, which are common for all KV1
454// variants.
455struct Kv1JourneyPattern {
456 struct Key {
457 // Mandatory (key), at most 10 characters. Transport operator (from list as
458 // defined in BISON enumeration E1).
459 std::string data_owner_code;
460 // Mandatory (key), at most 10 characters.
461 std::string line_planning_number;
462 // Mandatory (key), at most 10 characters.
463 std::string journey_pattern_code;
464
465 explicit Key(std::string data_owner_code,
466 std::string line_planning_number,
467 std::string journey_pattern_code);
468 };
469
470 Key key;
471 // Mandatory, at most 10 characters. Refers to a journey pattern type
472 // (JOPATYPE).
473 std::string journey_pattern_type;
474 // Mandatory, at most 1 character. One of [1, 2, A, B].
475 char direction = 0;
476 // Optional, at most 255 characters.
477 std::string description;
478
479 Kv1Line *p_line = nullptr;
480};
481
482// KV1 Table 10: Concession Financer Relation [CONFINREL]
483//
484// Concession financer relation (mainly parcel). Smallest unit for which data
485// about a concession can be captured in relation to a financer and/or
486// concession.
487//
488// This table is part of the core data tables, which are common for all KV1
489// variants.
490struct Kv1ConcessionFinancerRelation {
491 struct Key {
492 // Mandatory (key), at most 10 characters. Transport operator (from list as
493 // defined in BISON enumeration E1).
494 std::string data_owner_code;
495 // Mandatory (key), at most 10 characters. Parcel code.
496 std::string con_fin_rel_code;
497
498 explicit Key(std::string data_owner_code,
499 std::string con_fin_rel_code);
500 };
501
502 Key key;
503 // Mandatory, at most 10 characters. Concession code.
504 std::string concession_area_code;
505 // Optional, at most 10 characters. Code of financer/client of the parcel.
506 std::string financer_code;
507
508 Kv1ConcessionArea *p_concession_area = nullptr;
509 Kv1Financer *p_financer = nullptr;
510};
511
512// KV1 Table 11: Concession Area [CONAREA]
513//
514// Concession (area).
515//
516// This table is part of the core data tables, which are common for all KV1
517// variants.
518struct Kv1ConcessionArea {
519 struct Key {
520 // Mandatory (key), at most 10 characters. Transport operator (from list as
521 // defined in BISON enumeration E1).
522 std::string data_owner_code;
523 // Mandatory (key), at most 10 characters. Code of the concession.
524 std::string concession_area_code;
525
526 explicit Key(std::string data_owner_code,
527 std::string concession_area_code);
528 };
529
530 Key key;
531 // Mandatory, at most 255 characters.
532 std::string description;
533};
534
535// KV1 Table 12: Financer [FINANCER] (OPTIONAL)
536//
537// Financer of a parcel.
538//
539// This table is part of the core data tables, which are common for all KV1
540// variants.
541struct Kv1Financer {
542 struct Key {
543 // Mandatory (key), at most 10 characters. Transport operator (from list as
544 // defined in BISON enumeration E1).
545 std::string data_owner_code;
546 // Mandatory (key), at most 10 characters.
547 std::string financer_code;
548
549 explicit Key(std::string data_owner_code,
550 std::string financer_code);
551 };
552
553 Key key;
554 // Mandatory, at most 255 characters.
555 std::string description;
556};
557
558// KV1 Table 13: Journey Pattern Timing Link [JOPATILI]
559//
560// Compilation of journey pattern from logical links (between pairs of
561// stops/timing points). Features such as the destination code, the public line
562// number, the concession financer relation (parcel) and product formula are
563// set per connection. Moreover, a color and/or image linked to the line
564// destination and the use of the (first) stop as boarding/alighting stop can
565// be set per link.
566//
567// Timing Link: A timing link is a stop, set by the transit operator, where a
568// bus / public transit vehicle may never depart earlier than set in the
569// timetable.
570//
571// A logical link may never occur more than once in a journey pattern.
572// Therefore, the combination of LinePlanningNumber, JourneyPatternCode,
573// UserStopCodeBegin and UserStopCodeEnd must be unique in JOPATILI.
574//
575// The value of GetIn and GetOut are normally copied from the corresponding
576// stop in the USRSTOP table, but can be overruled per journey pattern if so
577// desired.
578//
579// A Icon or (Text)Color set here overrules the general value of the
580// corresponding line (Line) or destination (Destination).
581//
582// A value of ShowFlexibleTrip or ProductFormulaType in PUJO or PUJOPASS
583// overrules the value in JOPATILI.
584//
585// This table is part of the core data tables, which are common for all KV1
586// variants.
587struct Kv1JourneyPatternTimingLink {
588 struct Key {
589 // Mandatory (key), at most 10 characters. Transport operator (from list as
590 // defined in BISON enumeration E1).
591 std::string data_owner_code;
592 // Mandatory (key), at most 10 characters.
593 std::string line_planning_number;
594 // Mandatory (key), at most 10 characters.
595 std::string journey_pattern_code;
596 // Mandatory (key), at most 3 digits.
597 short timing_link_order = 0;
598
599 explicit Key(std::string data_owner_code,
600 std::string line_planning_number,
601 std::string journey_pattern_code,
602 short timing_link_order);
603 };
604
605 Key key;
606 // Mandatory, at most 10 characters. Stop number in the domain of the
607 // DataOwner (here: the transit operator).
608 std::string user_stop_code_begin;
609 // Mandatory, at most 10 characters. Stop number in the domain of the
610 // DataOwner (here: the transit operator).
611 std::string user_stop_code_end;
612 // Mandatory, at most 10 characters. Concession financer relation / parcel
613 // (smallest unit).
614 std::string con_fin_rel_code;
615 // Mandatory, at most 10 characters. The destination (incl. intermediat
616 // destinations) as these are shown at the first stop of the journey pattern
617 // link.
618 std::string dest_code;
619 // Mandatory, at most 5 characters. Boolean which indicates whether the first
620 // stop of the connection is a timing stop. Indicator is at least "true" at
621 // first stop of a line and at waiting stops.
622 bool is_timing_stop = false;
623 // Optional, at most 4 characters. Public line number which must be shown on
624 // displays from the first stop of the journey pattern link (e.g. Line number
625 // + S). This is important when a deviating public line number applies from a
626 // certain point on forward. Normally, the public line number of the
627 // corresponding line is shown.
628 std::string display_public_line;
629 // Optional, at most 4 digits. Enumeration E10 (see section 2.5). A public
630 // transit service which distinguishes itself by a set of unique features,
631 // that is offered to the passenger as distinct (a marketing aspect).
632 // TODO: Check if we can turn BISON enumeration E10 into an enum
633 std::optional<short> product_formula_type;
634 // Mandatory, at most 5 characters. Boolean indicator whether UserStopBegin
635 // is used as a boarding stop in this journey pattern. Usually equal to the
636 // value of the corresponding USRSTOP.
637 bool get_in = false;
638 // Mandatory, at most 5 characters. Boolean indicator whether UserStopBegin
639 // is used as an alighting stop in this journey pattern. Usually equal to the
640 // value of the corresponding USRSTOP.
641 bool get_out = false;
642 // Optional, at most 8 characters. Indicates whether the transit operator
643 // wants a not explicitly planned trip (i.e. a trip that only operates after
644 // reservation such as a 'call bus' (belbus), 'line taxi' (lijntaxi) etc.) to
645 // be shown on displays. Values according enumeration E21: TRUE (always),
646 // FALSE (never), REALTIME (only when tracking trip).
647 // TODO: Check if we can turn BISON enumeration E21 into an enum
648 std::string show_flexible_trip;
649 // Optional, at most 4 digits. Symbol / image for display of the line
650 // destination at the journey stop passing. Reference to the ICON table.
651 std::optional<short> line_dest_icon;
652 // Optional, at most 6 characters. Background color for display of the line
653 // destination at a journey stop passing. Hexadecimal representation
654 // following RGB coding. Always six characters (RRGGBB), only numbers and/or
655 // capital letters.
656 std::optional<RgbColor> line_dest_color;
657 // Optional, at most 6 characters. Foreground color for display of the line
658 // destination at a journey stop passing. Hexadecimal representation
659 // following RGB coding. Always six characters (RRGGBB), only numbers and/or
660 // capital letters.
661 std::optional<RgbColor> line_dest_text_color;
662
663 Kv1Line *p_line = nullptr;
664 Kv1JourneyPattern *p_journey_pattern = nullptr;
665 Kv1UserStopPoint *p_user_stop_begin = nullptr;
666 Kv1UserStopPoint *p_user_stop_end = nullptr;
667 Kv1ConcessionFinancerRelation *p_con_fin_rel = nullptr;
668 Kv1Destination *p_dest = nullptr;
669 Kv1Icon *p_line_dest_icon = nullptr;
670};
671
672// KV1 Table 14: Point [POINT]
673//
674// A point is the smallest location which can be reffered to within the public
675// transit network. Every stop (USRSTOP) is a point.
676//
677// This table is part of the core data tables, which are common for all KV1
678// variants.
679struct Kv1Point {
680 struct Key {
681 // Mandatory (key), at most 10 characters. Transport operator (from list as
682 // defined in BISON enumeration E1).
683 std::string data_owner_code;
684 // Mandatory (key), at most 10 characters.
685 std::string point_code;
686
687 explicit Key(std::string data_owner_code,
688 std::string point_code);
689 };
690
691 Key key;
692 // Mandatory, at most 10 characters. Refers to the POINTTYPE table.
693 std::string point_type;
694 // Mandatory, at most 10 characters. Refers to the GEOSYSTYPE table. Only
695 // allowed to have the value "RD" (rijkdsdriehoekstelsel; the national Dutch
696 // coordinate system).
697 std::string coordinate_system_type;
698 // Mandatory, at most 15 characters. X position in the RD coordinate system,
699 // in meters (at least 6 digits).
700 double location_x_ew = 0;
701 // Mandatory, at most 15 characters. Y position in the RD coordinate system,
702 // in meters (at least 6 digits).
703 double location_y_ns = 0;
704 // Optional, at most 15 characters.
705 // NOTE: the standart (presumeably wrongly) indicates this field as having
706 // alphanumeric contents.
707 std::optional<double> location_z;
708 // Optional, at most 255 characters.
709 std::string description;
710};
711
712// KV1 Table 15: Point on Link [POOL]
713//
714// A point that is used to geographically describe the trajectory between two
715// stops.
716//
717// This table is part of the core data tables, which are common for all KV1
718// variants.
719struct Kv1PointOnLink {
720 struct Key {
721 // Mandatory (key), at most 10 characters. Transport operator (from list as
722 // defined in BISON enumeration E1).
723 std::string data_owner_code;
724 // Mandatory (key), at most 10 characters. Stop number in the domain of the
725 // DataOwner (here: transit operator).
726 std::string user_stop_code_begin;
727 // Mandatory (key), at most 10 characters. Stop number in the domain of the
728 // DataOwner (here: transit operator).
729 std::string user_stop_code_end;
730 // Mandatory (key), at most 10 characters. Code from the road manager for KAR
731 // points. For curve points of the DataOwner (often the transit operator).
732 std::string point_data_owner_code;
733 // Mandatory (key), at most 10 charcters.
734 std::string point_code;
735 // Mandatory (key), at most 5 characters. Modality for which the distance
736 // applies, see BISON enumeration E9.
737 std::string transport_type;
738
739 explicit Key(std::string data_owner_code,
740 std::string user_stop_code_begin,
741 std::string user_stop_code_end,
742 std::string point_data_owner_code,
743 std::string point_code,
744 std::string transport_type);
745 };
746
747 Key key;
748 // Mandatory, at most 5 digits. Distance in meters relative to the start of
749 // the link.
750 double distance_since_start_of_link = 0;
751 // Optional, at most 4 digits. Crossing speed for a public transit vehicle
752 // from the previous point (on a link) in m/s.
753 std::optional<double> segment_speed_mps = 0;
754 // Optional, at most 4 digits. Comfort speed for a public transit vehicle on
755 // the curve point.
756 std::optional<double> local_point_speed_mps = 0;
757 // Optional, at most 255 characters.
758 std::string description;
759
760 Kv1UserStopPoint *p_user_stop_begin = nullptr;
761 Kv1UserStopPoint *p_user_stop_end = nullptr;
762 Kv1Point *p_point = nullptr;
763};
764
765// KV1 Table 16: Icon [ICON]
766//
767// Table with images which can be referred to from DEST.DestIcon, LINE.LineIcon
768// and JOPATILI.LineDestIcon to load the correct image.
769//
770// This table is part of the core data tables, which are common for all KV1
771// variants.
772struct Kv1Icon {
773 struct Key {
774 // Mandatory (key), at most 10 characters. Transport operator (from list as
775 // defined in BISON enumeration E1).
776 std::string data_owner_code;
777 // Mandatory (key), at most 4 digits. Reference from other tables for the
778 // requested image.
779 short icon_number = 0;
780
781 explicit Key(std::string data_owner_code,
782 short icon_number);
783 };
784
785 Key key;
786 // Mandatory, at most 1024 characters. Absolute URI to a publically available
787 // location from which the image can be loaded. The extension of the file
788 // indicates the image type.
789 // Supported file types are: GIF (.gif), JPEG (.jpg, .jpeg),
790 // PNG (.png), SVG (.svg)
791 // Supported protocols are: HTTP, HTTPS, FTP
792 // Prefer to not use any capital letters. Examples:
793 // - http://bison.dova.nu/images/logo.png
794 // - https://bison.dova.nu/images/logo.png
795 // - ftp://ftp.dova.nu/images/logo.png
796 std::string icon_uri;
797};
798
799// KV1 Table 17: Notice [NOTICE] (OPTIONAL)
800//
801// A (reusable) text with supplementary information about exceptions /
802// clarifications for a line, journey pattern etc.
803//
804// Usage is optional; when there are no clarifying texts, the NOTICE table does
805// not need to be provided in a KV1 set.
806//
807// This table is part of the core data tables, which are common for all KV1
808// variants.
809struct Kv1Notice {
810 struct Key {
811 // Mandatory (key), at most 10 characters. Transport operator (from list as
812 // defined in BISON enumeration E1).
813 std::string data_owner_code;
814 // Mandatory (key), at most 20 characters. Identification of Notice (remark,
815 // clarifying text).
816 std::string notice_code;
817
818 explicit Key(std::string data_owner_code,
819 std::string notice_code);
820 };
821
822 Key key;
823 // Mandatory, at most 1024 characters. Content, text. Contains contact
824 // information such as telephone number, web address and reservation time for
825 // 'call buses' (belbussen) and other demand-based transit.
826 std::string notice_content;
827};
828
829// KV1 Table 18: Notice Assignment [NTCASSGNM] (OPTIONAL)
830//
831// Linking table in which Notice (remark, clarfiying text) is assigned to a
832// line, journey pattern, stops within a journey pattern, journey etc. Notice
833// Assignment contains all logical key elements of the corresponding objects to
834// which a Notice can be assigned.
835//
836// Different attributes are required for the Notice Assignment, depending on
837// the type object to which the Notice is assigned. In the following table
838// structure, this is indicated as 'Only relevant for ...'. This means that
839// fields for other object types in the Notice Assignment can be ignored.
840//
841// Moreover, it can also occur that not all key fields of the linked table are
842// of interest (content-wise) for recording the Notice.
843//
844// Both matters are summarised in this overview:
845//
846// --------------------------------------------------------
847// AssignedObject PUJO PUJOPASS LINE JOPATILI
848// --------------------------------------------------------
849// DataOwnerCode........... x ...... x ...... x ..... x ...
850// TimetableVersionCode ... o .............................
851// OrganizationalUnitCode . o ...... o ....................
852// ScheduleCode .................... o ....................
853// ScheduleTypeCode ................ o ....................
854// PeriodGroupCode ........ o .............................
855// SpecificDayCode ........ o .............................
856// DayType ................ o .............................
857// LinePlanningNumber ..... x ...... x ...... x ..... x ...
858// JourneyNumber .......... x ...... x ....................
859// StopOrder ....................... o .............. o ...
860// JourneyPatternCode ............................... x ...
861// TimingLinkOrder .................................. o ...
862// UserStopCode .................... o .............. o ...
863// --------------------------------------------------------
864//
865// Legend:
866// x - Mandatory. The Notice for this object type is always depndent on the
867// value of the attribute.
868// o - Optional. The Notice can be independent of the value of this
869// attribute for this object type.
870// <empty> - Attribute is no key field for this object type and can be
871// ignored when processed.
872//
873// Usage of Notice Assignment is optional in KV1. If there are no clarifying
874// texts, then the Notice Assignment table is not required to be present in the
875// provided KV1 set.
876//
877// This table is part of the core data tables, which are common for all KV1
878// variants.
879struct Kv1NoticeAssignment {
880 // Mandatory, at most 10 characters. Transport operator (from list as
881 // defined in BISON enumeration E1).
882 std::string data_owner_code;
883 // Mandatory, at most 20 characters. Notice that is assigned.
884 std::string notice_code;
885 // Mandatory, at most 8 characters. Object type to which Notice is assigned.
886 std::string assigned_object;
887 // Optional, at most 10 characters. Only relevant for PUJO.
888 std::string timetable_version_code;
889 // Optional, at most 10 characters. Only relevant for PUJO and PUJOPASS.
890 std::string organizational_unit_code;
891 // Optional, at most 10 characters. Only relevant for PUJOPASS.
892 std::string schedule_code;
893 // Optional, at most 10 characters. Only relevant for PUJOPASS.
894 std::string schedule_type_code;
895 // Optional, at most 10 characters. Only relevant for PUJO.
896 std::string period_group_code;
897 // Optional, at most 10 characters. Only relevant for PUJO.
898 std::string specific_day_code;
899 // Optional, at most 10 characters. Only relevant for PUJO.
900 // [0|1][0|2][0|3][0|4][0|5][0|6][0|7] for Mon, Tue, Wed, Thu, Fri, Sat, Sun.
901 // E.g. 1234500 means Mon, Tue, Wed, Thu, Fri but not Sat, Sun.
902 std::string day_type;
903 // Mandatory, at most 10 characters. Mandatory for all object types.
904 std::string line_planning_number;
905 // Optional (for all object types except PUJO and PUJOPASS), at most 6
906 // digits. Only relevant for PUJO and PUJOPASS. Must be in the range
907 // [0-1000000).
908 std::optional<int> journey_number;
909 // Optional, at most 4 digits. Only relevant for PUJOPASS and JOPATILI.
910 std::optional<int> stop_order;
911 // Optional (for all object types except JOPATILI), at most 4 digits. Only
912 // relevant for JOPATILI.
913 std::string journey_pattern_code;
914 // Optional (at most 3 digits). Only relevant for JOPATILI.
915 std::optional<short> timing_link_order;
916 // Optional (at most 10 characters). Only relevant for PUJOPASS and JOPATILI.
917 // For JOPATILI, this correspond to the first stop of the link.
918 std::string user_stop_code;
919
920 Kv1Notice *p_notice = nullptr;
921};
922
923// KV1 Table 19: Time Demand Group [TIMDEMGRP]
924//
925// A time demand group is a grouping of the run time distribution from stop to
926// stop, for a journey pattern (from start to end point).
927//
928// This table is part of the KV1 variant "validities and time demand groups".
929struct Kv1TimeDemandGroup {
930 struct Key {
931 // Mandatory (key), at most 10 characters. Transport operator (from list as
932 // defined in BISON enumeration E1).
933 std::string data_owner_code;
934 // Mandatory (key), at most 10 characters.
935 std::string line_planning_number;
936 // Mandatory (key), at most 10 characters. Refers to the JOPATILI table.
937 std::string journey_pattern_code;
938 // Mandatory (key), at most 10 characters. Defines the code for the time
939 // demand group. (NOTE: this is not entirely made clear by the specification.
940 // This claim must be verified.)
941 std::string time_demand_group_code;
942
943 explicit Key(std::string data_owner_code,
944 std::string line_planning_number,
945 std::string journey_pattern_code,
946 std::string time_demand_group_code);
947 };
948
949 Key key;
950
951 Kv1Line *p_line = nullptr;
952 Kv1JourneyPattern *p_journey_pattern = nullptr;
953};
954
955// KV1 Table 20: Time Demand Group Run Time [TIMDEMRNT]
956//
957// The run time structure/distribution for all timing links of a journey
958// pattern or a time demand group.
959//
960// Optional run time elements are, when these are present, used to more
961// accurately calculate expected departure times based on punctuality
962// deviations.
963//
964// This table is part of the KV1 variant "validities and time demand groups".
965struct Kv1TimeDemandGroupRunTime {
966 struct Key {
967 // Mandatory (key), at most 10 characters. Transport operator (from list as
968 // defined in BISON enumeration E1).
969 std::string data_owner_code;
970 // Mandatory (key), at most 10 characters.
971 std::string line_planning_number;
972 // Mandatory (key), at most 10 characters. Refers to the JOPATILI table.
973 std::string journey_pattern_code;
974 // Mandatory (key), at most 10 characters. Refers to the TIMDEMGRP table.
975 std::string time_demand_group_code;
976 // Mandatory (key), at most 3 digits. Reference number of a link within the
977 // journey pattern (a link can occur more than once within a journey
978 // pattern).
979 short timing_link_order = 0;
980
981 explicit Key(std::string data_owner_code,
982 std::string line_planning_number,
983 std::string journey_pattern_code,
984 std::string time_demand_group_code,
985 short timing_link_order);
986 };
987
988 Key key;
989 // Mandatory, at most 10 characters. Refers to the first stop of the link.
990 std::string user_stop_code_begin;
991 // Mandatory, at most 10 characters. Refers to the last stop of the link.
992 std::string user_stop_code_end;
993 // Mandatory, at most 5 digits. Planned total run time on link for time
994 // demand group: (Departure time end stop - departure time begin stop)
995 // corresponding to the time demand group. In seconds.
996 double total_drive_time_s = 0;
997 // Mandatory, at most 5 digits. Planned minimal run time on link for time
998 // demand group. Often calculated as: (Arrival time end stop - arrival time
999 // begin stop) corresponding to the time demand group. In seconds.
1000 double drive_time_s = 0;
1001 // Optional, at most 5 digits. Expected/planned delay/congestion on link for
1002 // time demand group. In seconds.
1003 std::optional<double> expected_delay_s;
1004 // Optional, at most 5 digits. Layover/catch-up time. Gives play in the
1005 // timetable. In seconds.
1006 // LayOverTime = TotDriveTime - DriveTime + ExpectedDelay - StopWaitTime.
1007 std::optional<double> layover_time;
1008 // Mandatory, at most 5 digits. Planned stop waiting time at the final stop
1009 // of the link for the time demand group. Determined based on the difference
1010 // between the departure time and arrival time at this stop. Is zero when no
1011 // waiting time is planned for this stop. In seconds.
1012 double stop_wait_time = 0;
1013 // Optional, at most 5 digits. Planned minimal stop time for
1014 // boarding/alighting of passengers at the final stop of the link for the
1015 // time demand group. Application: at hub stops with a planned waiting time,
1016 // the difference between the planned waiting time and the minimum stop time
1017 // is the layover/catch-up time. In seconds.
1018 std::optional<double> minimum_stop_time;
1019
1020 Kv1Line *p_line = nullptr;
1021 Kv1UserStopPoint *p_user_stop_begin = nullptr;
1022 Kv1UserStopPoint *p_user_stop_end = nullptr;
1023 Kv1JourneyPattern *p_journey_pattern = nullptr;
1024 Kv1TimeDemandGroup *p_time_demand_group = nullptr;
1025 Kv1JourneyPatternTimingLink *p_journey_pattern_timing_link = nullptr;
1026};
1027
1028// KV1 Table 21: Period Group [PEGR]
1029//
1030// Period group is an indication of a 'homogeneous period' during the year,
1031// i.e. a period in which the schedule has the same composition w.r.t.
1032// frequencies and run times.
1033//
1034// This table is part of the KV1 variant "validities and time demand groups".
1035struct Kv1PeriodGroup {
1036 struct Key {
1037 // Mandatory (key), at most 10 characters. Transport operator (from list as
1038 // defined in BISON enumeration E1).
1039 std::string data_owner_code;
1040 // Mandatory (key), at most 10 characters.
1041 std::string period_group_code;
1042
1043 explicit Key(std::string data_owner_code,
1044 std::string period_group_code);
1045 };
1046
1047 Key key;
1048 // Optional, at most 255 characters.
1049 std::string description;
1050};
1051
1052// KV1 Table 22: Specific Day [SPECDAY]
1053//
1054// A specific day is a feature of a day for which a deviating service level is
1055// provided, respective to a normal day of the week.
1056//
1057// E.g. shopping Sundays (koopzondagen, if not every Sunday), New Year's Eve
1058// (oudejaarsdag), foreign bank holidays (as applicable).
1059//
1060// This table is part of the KV1 variant "validities and time demand groups".
1061struct Kv1SpecificDay {
1062 struct Key {
1063 // Mandatory (key), at most 10 characters. Transport operator (from list as
1064 // defined in BISON enumeration E1).
1065 std::string data_owner_code;
1066 // Mandatory (key), at most 10 characters. Default: "NORMAL".
1067 std::string specific_day_code;
1068
1069 explicit Key(std::string data_owner_code,
1070 std::string specific_day_code);
1071 };
1072
1073 Key key;
1074 // Mandatory, at most 50 characters.
1075 std::string name;
1076 // Optional, at most 255 characters.
1077 std::string description;
1078};
1079
1080// KV1 Table 23: Timetable Version [TIVE]
1081//
1082// A timetable version budles all planned activities for an organizational
1083// unit. For the public schedule, these are trips, routes, run times etc.
1084//
1085// When processing a new Timetable Version, it is checked if another TIVE with
1086// the same key has already been processed. If this is the case, ValidFrom must
1087// be equal to the starting date of the previously provided set. The new set
1088// replaces the older one. A package with a new starting date is only processed
1089// if another TimetableVersionCode is used.
1090//
1091// This table is part of the KV1 variant "validities and time demand groups".
1092struct Kv1TimetableVersion {
1093 struct Key {
1094 // Mandatory (key), at most 10 characters. Transport operator (from list as
1095 // defined in BISON enumeration E1).
1096 std::string data_owner_code;
1097 // Mandatory (key), at most 10 characters.
1098 std::string organizational_unit_code;
1099 // Mandatory (key), at most 10 characters.
1100 std::string timetable_version_code;
1101 // Mandatory (key), at most 10 charactes.
1102 std::string period_group_code;
1103 // Mandatory (key), at most 10 characters. Default: "NORMAL".
1104 std::string specific_day_code;
1105
1106 explicit Key(std::string data_owner_code,
1107 std::string organizational_unit_code,
1108 std::string timetable_version_code,
1109 std::string period_group_code,
1110 std::string specific_day_code);
1111 };
1112
1113 Key key;
1114 // Mandatory, at most 10 characters. Datum on which the timetable goes into
1115 // effect, following the YYYY-MM-DD format.
1116 std::chrono::year_month_day valid_from;
1117 // Mandatory, at most 10 characters. Value: "PUBT".
1118 std::string timetable_version_type;
1119 // Optional, at most 10 characters. Datum on which the timetable goes out of
1120 // effect, following the YYYY-MM-DD format.
1121 std::optional<std::chrono::year_month_day> valid_thru;
1122 // Optional, at most 255 characters. Should be null/empty.
1123 std::string description;
1124
1125 Kv1OrganizationalUnit *p_organizational_unit = nullptr;
1126 Kv1PeriodGroup *p_period_group = nullptr;
1127 Kv1SpecificDay *p_specific_day = nullptr;
1128};
1129
1130// KV1 Table 24: Public Journey [PUJO]
1131//
1132// Public journeys are journeys that are operated by a public transit
1133// organization and are accessible to the passenger.
1134//
1135// Business rules:
1136// - If ShowFlexibleTrip or ProductFormulaType is set in a record of this
1137// table, this takes precedence over the value as in the corresponding
1138// JOPATILI entry.
1139//
1140// This table is part of the KV1 variant "validities and time demand groups".
1141struct Kv1PublicJourney {
1142 struct Key {
1143 // Mandatory (key), at most 10 characters. Transport operator (from list as
1144 // defined in BISON enumeration E1).
1145 std::string data_owner_code;
1146 // Mandatory (key), at most 10 characters.
1147 std::string timetable_version_code;
1148 // Mandatory (key), at most 10 characters.
1149 std::string organizational_unit_code;
1150 // Mandatory (key), at most 10 characters.
1151 std::string period_group_code;
1152 // Mandatory (key), at most 10 characters.
1153 std::string specific_day_code;
1154 // Mandatory (key), at most 7 characters.
1155 // [0|1][0|2][0|3][0|4][0|5][0|6][0|7] for Mon, Tue, Wed, Thu, Fri, Sat, Sun.
1156 // E.g. 1234500 means Mon, Tue, Wed, Thu, Fri but not Sat, Sun.
1157 // TODO: See if we can make this into a more concrete type
1158 std::string day_type;
1159 // Mandatory (key), at most 10 characters.
1160 std::string line_planning_number;
1161 // Mandatory (key), at most 6 digits. Must be in the range [0-1000000).
1162 int journey_number = 0;
1163
1164 explicit Key(std::string data_owner_code,
1165 std::string timetable_version_code,
1166 std::string organizational_unit_code,
1167 std::string period_group_code,
1168 std::string specific_day_code,
1169 std::string day_type,
1170 std::string line_planning_number,
1171 int journey_number);
1172 };
1173
1174 Key key;
1175 // Mandatory, at most 10 characters.
1176 std::string time_demand_group_code;
1177 // Mandatory, at most 10 characters.
1178 std::string journey_pattern_code;
1179 // Mandatory, at most 8 characters. Format: "HH:MM:SS".
1180 std::chrono::hh_mm_ss<std::chrono::seconds> departure_time;
1181 // Mandatory, at most 13 characters. Values as in BISON enumeration E3.
1182 // Allowed are: "ACCESSIBLE", "NOTACCESSIBLE" and "UNKNOWN".
1183 // TODO: See if we can fit BISON enumeration E3 into an enum
1184 std::string wheelchair_accessible;
1185 // Mandatory, at most 5 characters. Boolean. Value "true": journey is
1186 // operator by DataOwner. Value "false": journey is operator by a different
1187 // DataOwner. Indicator is meant for a line that is operated jointly by
1188 // multiple transit operators. The indicator is used to be able to match the
1189 // journey operation (KV6, KV19 etc.); only journeys for which the indicator
1190 // is "true" can be expected to have corresponding current/real-time
1191 // information, although "true" doesn't necessarily mean that this
1192 // current/real-time information will (always) become available.
1193 bool data_owner_is_operator = false;
1194 // Mandatory, at most 5 characters. Boolean. Indicates whether
1195 // current/real-time journey information may be expected for the
1196 // corresponding journey ("true" or "false").
1197 bool planned_monitored = false;
1198 // Optional, at most 4 digits. BISON enumeration E10. Intended to allow
1199 // capturing transit mode features at the journey level.
1200 // TODO: See if we can make BISON enumeration E10 into an enum
1201 std::optional<short> product_formula_type;
1202 // Optional, at most 8 characters. Indicates whether the transit operator
1203 // wants that a not-explicitly planned trip (i.e. a journey that only runs on
1204 // reservation, e.g. 'call bus' (belbus), 'line taxi' (lijntaxi) etc.) to be
1205 // shown on displays. Values following BISON enumeration E21: TRUE (always),
1206 // FALSE (never), REALTIME (only when journey is tracked).
1207 // TODO: See if we can make BISON enumeration E21 into an enum
1208 std::string show_flexible_trip;
1209
1210 Kv1TimetableVersion *p_timetable_version = nullptr;
1211 Kv1OrganizationalUnit *p_organizational_unit = nullptr;
1212 Kv1PeriodGroup *p_period_group = nullptr;
1213 Kv1SpecificDay *p_specific_day = nullptr;
1214 Kv1Line *p_line = nullptr;
1215 Kv1TimeDemandGroup *p_time_demand_group = nullptr;
1216 Kv1JourneyPattern *p_journey_pattern = nullptr;
1217};
1218
1219// KV1 Table 25: Period Group Validity [PEGRVAL]
1220//
1221// Validities (multiple from-thru data) of a period group.
1222//
1223// This table is part of the KV1 variant "validities and time demand groups".
1224struct Kv1PeriodGroupValidity {
1225 struct Key {
1226 // Mandatory (key), at most 10 characters. Transport operator (from list as
1227 // defined in BISON enumeration E1).
1228 std::string data_owner_code;
1229 // Mandatory (key), at most 10 characters.
1230 std::string organizational_unit_code;
1231 // Mandatory (key), at most 10 characters.
1232 std::string period_group_code;
1233 // Mandatory (key), at most 10 characters. Date of the start of the validity
1234 // period. Format: "YYYY-MM-DD".
1235 std::chrono::year_month_day valid_from;
1236
1237 explicit Key(std::string data_owner_code,
1238 std::string organizational_unit_code,
1239 std::string period_group_code,
1240 std::chrono::year_month_day valid_from);
1241 };
1242
1243 Key key;
1244 // Mandatory, at most 10 characters. Date of the end of the validity period.
1245 // Format: "YYYY-MM-DD".
1246 std::chrono::year_month_day valid_thru;
1247
1248 Kv1OrganizationalUnit *p_organizational_unit = nullptr;
1249 Kv1PeriodGroup *p_period_group = nullptr;
1250};
1251
1252// KV1 Table 26: Exceptional Operating Day [EXCOPDAY]
1253//
1254// Contains exceptional validity dates, for which the service runs following a
1255// different day type (such as another day of the week or a different period).
1256//
1257// This table is part of the KV1 variant "validities and time demand groups".
1258struct Kv1ExceptionalOperatingDay {
1259 struct Key {
1260 // Mandatory (key), at most 10 characters. Transport operator (from list as
1261 // defined in BISON enumeration E1).
1262 std::string data_owner_code;
1263 // Mandatory (key), at most 10 characters. Organization unit for which an
1264 // exceptional day validity applies.
1265 std::string organizational_unit_code;
1266 // Mandatory (key), at most 23 characters. Date (+ time) for which the
1267 // exceptional validity applies. Format: "YYYYMMDDThh:mm:ssTZD".
1268 std::chrono::sys_seconds valid_date;
1269
1270 explicit Key(std::string data_owner_code,
1271 std::string organizational_unit_code,
1272 std::chrono::sys_seconds valid_date);
1273 };
1274
1275 Key key;
1276 // Mandatory, at most 7 characters. The exceptional day type that applies on
1277 // a calendar day: [0|1][0|2][0|3][0|4][0|5][0|6][0|7] for Mon, Tue, Wed,
1278 // Thu, Fri, Sat.
1279 // E.g. 1234500 means Mon, Tue, Wed, Thu, Fri but not Sat, Sun.
1280 // TODO: See if we can make this into a more concrete type
1281 std::string day_type_as_on;
1282 // Mandatory, at most 10 characters. Specific day service level to which the
1283 // exceptional day validity refers.
1284 std::string specific_day_code;
1285 // Optional, at most 10 characters. An exceptional day validity can be
1286 // related to the service level of another period (e.g. the school holiday
1287 // schedule). This exceptional period reference is set here.
1288 //
1289 // E.g. on Good Friday or the day after Ascension day, transit runs according
1290 // to the holiday season schedule, while transit runs following the winter
1291 // package in the surrounding days.
1292 std::string period_group_code;
1293 // Optional, at most 255 characters.
1294 std::string description;
1295
1296 Kv1OrganizationalUnit *p_organizational_unit = nullptr;
1297 Kv1SpecificDay *p_specific_day = nullptr;
1298 Kv1PeriodGroup *p_period_group = nullptr;
1299};
1300
1301// KV1 Table 27: Schedule Version [SCHEDVERS]
1302//
1303// A schedule version bundles the planned activities for an organisation unit
1304// per day type. The journeys with passing times and corresponding routes are
1305// for the public timetable.
1306//
1307// When processing a new Schedule Version, it is checked if another SCHEDVERS
1308// with the same key has already been processed. If this is the case, ValidFrom
1309// must be equal to the starting date of the previously provided set. The new
1310// set replaces the older one. A package with a new starting date is only
1311// processed if another Schedule Code is used.
1312//
1313// This table is part of the KV1 variant "schedules and passing times".
1314struct Kv1ScheduleVersion {
1315 struct Key {
1316 // Mandatory (key), at most 10 characters. Transport operator (from list as
1317 // defined in BISON enumeration E1).
1318 std::string data_owner_code;
1319 // Mandatory (key), at most 10 characters.
1320 std::string organizational_unit_code;
1321 // Mandatory (key), at most 10 characters. A unique code in combination with
1322 // the ScheduleTypeCode of the package within the ORUN.
1323 std::string schedule_code;
1324 // Mandatory (key), at most 10 characters. Code for the Schedule Type (Day Type).
1325 std::string schedule_type_code;
1326
1327 explicit Key(std::string data_owner_code,
1328 std::string organizational_unit_code,
1329 std::string schedule_code,
1330 std::string schedule_type_code);
1331 };
1332
1333 Key key;
1334 // Mandatory, at most 10 characters. Date on which the schedule goes into
1335 // effect. Format: "YYYY-MM-DD".
1336 std::chrono::year_month_day valid_from;
1337 // Optional, at most 10 characters. Date on which the schedule goes out of
1338 // effect. Format: "YYYY-MM-DD".
1339 std::optional<std::chrono::year_month_day> valid_thru;
1340 // Optional, at most 255 characters. Should be empty/null.
1341 std::string description;
1342
1343 Kv1OrganizationalUnit *p_organizational_unit = nullptr;
1344};
1345
1346// KV1 Table 28: Public Journey Passing Times [PUJOPASS]
1347//
1348// Public journey with arrival and departure times at all stops (and other
1349// timing points).
1350//
1351// Business rules:
1352// - If ShowFlexibleTrip or ProductFormulaType is set here, then this takes
1353// precedence over the value in the corresponding JOPATILI record.
1354// - All stop passings of a public journey refer to the same journey pattern
1355// (JOPA)!
1356//
1357// This table is part of the KV1 variant "schedules and passing times".
1358struct Kv1PublicJourneyPassingTimes {
1359 struct Key {
1360 // Mandatory (key), at most 10 characters. Transport operator (from list as
1361 // defined in BISON enumeration E1).
1362 std::string data_owner_code;
1363 // Mandatory (key), at most 10 characters.
1364 std::string organizational_unit_code;
1365 // Mandatory (key), at most 10 characters. A unique code in combination with
1366 // the ScheduleTypeCode of the package within the ORUN.
1367 std::string schedule_code;
1368 // Mandatory (key), at most 10 characters. Code for the Schedule Type (e.g.
1369 // Day Type).
1370 std::string schedule_type_code;
1371 // Mandatory (key), at most 10 characters.
1372 std::string line_planning_number;
1373 // Mandatory (key), at most 6 digits. Must be in the range [0-1000000).
1374 int journey_number = 0;
1375 // Mandatory (key), at most 4 digits.
1376 short stop_order = 0;
1377
1378 explicit Key(std::string data_owner_code,
1379 std::string organizational_unit_code,
1380 std::string schedule_code,
1381 std::string schedule_type_code,
1382 std::string line_planning_number,
1383 int journey_number,
1384 short stop_order);
1385 };
1386
1387 Key key;
1388 // Mandatory, at most 10 characters.
1389 std::string journey_pattern_code;
1390 // Mandatory, at most 10 characters.
1391 std::string user_stop_code;
1392 // Mandatory (except for the first stop of a journey), at most 8 digits. Not
1393 // compulsory for the first stop of a journey. Format: "HH:MM:SS".
1394 std::optional<std::chrono::hh_mm_ss<std::chrono::seconds>> target_arrival_time;
1395 // Mandatory (expect for the last stop of a journey), at most 8 digits. Not
1396 // compulsory for the last stop of a journey. Format: "HH:MM:SS".
1397 std::optional<std::chrono::hh_mm_ss<std::chrono::seconds>> target_departure_time;
1398 // Mandatory, at most 13 characters. Values as in BISON enumeration E3.
1399 // Allowed are: "ACCESSIBLE", "NOTACCESSIBLE" and "UNKNOWN".
1400 // TODO: See if we can fit BISON enumeration E3 into an enum
1401 std::string wheelchair_accessible;
1402 // Mandatory, at most 5 characters. Boolean. Value "true": journey is
1403 // operator by DataOwner. Value "false": journey is operator by a different
1404 // DataOwner. Indicator is meant for a line that is operated jointly by
1405 // multiple transit operators. The indicator is used to be able to match the
1406 // journey operation (KV6, KV19 etc.); only journeys for which the indicator
1407 // is "true" can be expected to have corresponding current/real-time
1408 // information, although "true" doesn't necessarily mean that this
1409 // current/real-time information will (always) become available.
1410 bool data_owner_is_operator = false;
1411 // Mandatory, at most 5 characters. Boolean. Indicates whether
1412 // current/real-time journey information may be expected for the
1413 // corresponding journey ("true" or "false").
1414 bool planned_monitored = false;
1415 // Optional, at most 4 digits. BISON enumeration E10. Intended to allow
1416 // capturing transit mode features at the journey level.
1417 // TODO: See if we can make BISON enumeration E10 into an enum
1418 std::optional<short> product_formula_type;
1419 // Optional, at most 8 characters. Indicates whether the transit operator
1420 // wants that a not-explicitly planned trip (i.e. a journey that only runs on
1421 // reservation, e.g. 'call bus' (belbus), 'line taxi' (lijntaxi) etc.) to be
1422 // shown on displays. Values following BISON enumeration E21: TRUE (always),
1423 // FALSE (never), REALTIME (only when journey is tracked).
1424 // TODO: See if we can make BISON enumeration E21 into an enum
1425 std::string show_flexible_trip;
1426
1427 Kv1OrganizationalUnit *p_organizational_unit = nullptr;
1428 Kv1ScheduleVersion *p_schedule_version = nullptr;
1429 Kv1Line *p_line = nullptr;
1430 Kv1JourneyPattern *p_journey_pattern = nullptr;
1431 Kv1UserStopPoint *p_user_stop = nullptr;
1432};
1433
1434// KV1 Table 29: Operating Day [OPERDAY]
1435//
1436// Contains the operational calendar. Which package (schedule version) applies
1437// is specified per day, per organisation unit.
1438//
1439// This table is part of the KV1 variant "schedules and passing times".
1440struct Kv1OperatingDay {
1441 struct Key {
1442 // Mandatory (key), at most 10 characters. Transport operator (from list as
1443 // defined in BISON enumeration E1).
1444 std::string data_owner_code;
1445 // Mandatory (key), at most 10 characters.
1446 std::string organizational_unit_code;
1447 // Mandatory (key), at most 10 characters.
1448 std::string schedule_code;
1449 // Mandatory (key), at most 10 characters.
1450 std::string schedule_type_code;
1451 // Mandatory (key), at most 10 characters. Date on which the package
1452 // (schedule version) applies. Format: "YYYY-MM-DD".
1453 std::chrono::year_month_day valid_date;
1454
1455 explicit Key(std::string data_owner_code,
1456 std::string organizational_unit_code,
1457 std::string schedule_code,
1458 std::string schedule_type_code,
1459 std::chrono::year_month_day valid_date);
1460 };
1461
1462 Key key;
1463 // Optional, at most 255 characters.
1464 std::string description;
1465
1466 Kv1OrganizationalUnit *p_organizational_unit = nullptr;
1467 Kv1ScheduleVersion *p_schedule_version = nullptr;
1468};
1469
1470bool operator==(const Kv1OrganizationalUnit::Key &a, const Kv1OrganizationalUnit::Key &b);
1471bool operator==(const Kv1HigherOrganizationalUnit::Key &a, const Kv1HigherOrganizationalUnit::Key &b);
1472bool operator==(const Kv1UserStopPoint::Key &a, const Kv1UserStopPoint::Key &b);
1473bool operator==(const Kv1UserStopArea::Key &a, const Kv1UserStopArea::Key &b);
1474bool operator==(const Kv1TimingLink::Key &a, const Kv1TimingLink::Key &b);
1475bool operator==(const Kv1Link::Key &a, const Kv1Link::Key &b);
1476bool operator==(const Kv1Line::Key &a, const Kv1Line::Key &b);
1477bool operator==(const Kv1Destination::Key &a, const Kv1Destination::Key &b);
1478bool operator==(const Kv1JourneyPattern::Key &a, const Kv1JourneyPattern::Key &b);
1479bool operator==(const Kv1ConcessionFinancerRelation::Key &a, const Kv1ConcessionFinancerRelation::Key &b);
1480bool operator==(const Kv1ConcessionArea::Key &a, const Kv1ConcessionArea::Key &b);
1481bool operator==(const Kv1Financer::Key &a, const Kv1Financer::Key &b);
1482bool operator==(const Kv1JourneyPatternTimingLink::Key &a, const Kv1JourneyPatternTimingLink::Key &b);
1483bool operator==(const Kv1Point::Key &a, const Kv1Point::Key &b);
1484bool operator==(const Kv1PointOnLink::Key &a, const Kv1PointOnLink::Key &b);
1485bool operator==(const Kv1Icon::Key &a, const Kv1Icon::Key &b);
1486bool operator==(const Kv1Notice::Key &a, const Kv1Notice::Key &b);
1487bool operator==(const Kv1TimeDemandGroup::Key &a, const Kv1TimeDemandGroup::Key &b);
1488bool operator==(const Kv1TimeDemandGroupRunTime::Key &a, const Kv1TimeDemandGroupRunTime::Key &b);
1489bool operator==(const Kv1PeriodGroup::Key &a, const Kv1PeriodGroup::Key &b);
1490bool operator==(const Kv1SpecificDay::Key &a, const Kv1SpecificDay::Key &b);
1491bool operator==(const Kv1TimetableVersion::Key &a, const Kv1TimetableVersion::Key &b);
1492bool operator==(const Kv1PublicJourney::Key &a, const Kv1PublicJourney::Key &b);
1493bool operator==(const Kv1PeriodGroupValidity::Key &a, const Kv1PeriodGroupValidity::Key &b);
1494bool operator==(const Kv1ExceptionalOperatingDay::Key &a, const Kv1ExceptionalOperatingDay::Key &b);
1495bool operator==(const Kv1ScheduleVersion::Key &a, const Kv1ScheduleVersion::Key &b);
1496bool operator==(const Kv1PublicJourneyPassingTimes::Key &a, const Kv1PublicJourneyPassingTimes::Key &b);
1497bool operator==(const Kv1OperatingDay::Key &a, const Kv1OperatingDay::Key &b);
1498
1499size_t hash_value(const Kv1OrganizationalUnit::Key &k);
1500size_t hash_value(const Kv1HigherOrganizationalUnit::Key &k);
1501size_t hash_value(const Kv1UserStopPoint::Key &k);
1502size_t hash_value(const Kv1UserStopArea::Key &k);
1503size_t hash_value(const Kv1TimingLink::Key &k);
1504size_t hash_value(const Kv1Link::Key &k);
1505size_t hash_value(const Kv1Line::Key &k);
1506size_t hash_value(const Kv1Destination::Key &k);
1507size_t hash_value(const Kv1JourneyPattern::Key &k);
1508size_t hash_value(const Kv1ConcessionFinancerRelation::Key &k);
1509size_t hash_value(const Kv1ConcessionArea::Key &k);
1510size_t hash_value(const Kv1Financer::Key &k);
1511size_t hash_value(const Kv1JourneyPatternTimingLink::Key &k);
1512size_t hash_value(const Kv1Point::Key &k);
1513size_t hash_value(const Kv1PointOnLink::Key &k);
1514size_t hash_value(const Kv1Icon::Key &k);
1515size_t hash_value(const Kv1Notice::Key &k);
1516size_t hash_value(const Kv1TimeDemandGroup::Key &k);
1517size_t hash_value(const Kv1TimeDemandGroupRunTime::Key &k);
1518size_t hash_value(const Kv1PeriodGroup::Key &k);
1519size_t hash_value(const Kv1SpecificDay::Key &k);
1520size_t hash_value(const Kv1TimetableVersion::Key &k);
1521size_t hash_value(const Kv1PublicJourney::Key &k);
1522size_t hash_value(const Kv1PeriodGroupValidity::Key &k);
1523size_t hash_value(const Kv1ExceptionalOperatingDay::Key &k);
1524size_t hash_value(const Kv1ScheduleVersion::Key &k);
1525size_t hash_value(const Kv1PublicJourneyPassingTimes::Key &k);
1526size_t hash_value(const Kv1OperatingDay::Key &k);
1527
1528#endif // OEUF_LIBTMI8_KV1_TYPES_HPP
diff --git a/lib/libtmi8/include/tmi8/kv6_parquet.hpp b/lib/libtmi8/include/tmi8/kv6_parquet.hpp
new file mode 100644
index 0000000..33b57ca
--- /dev/null
+++ b/lib/libtmi8/include/tmi8/kv6_parquet.hpp
@@ -0,0 +1,46 @@
1// vim:set sw=2 ts=2 sts et:
2
3#ifndef OEUF_LIBTMI8_KV6_PARQUET_HPP
4#define OEUF_LIBTMI8_KV6_PARQUET_HPP
5
6#include <filesystem>
7
8#include <arrow/api.h>
9#include <arrow/io/api.h>
10#include <parquet/arrow/writer.h>
11
12static const size_t MAX_PARQUET_CHUNK = 10000;
13
14struct ParquetBuilder {
15 ParquetBuilder();
16 arrow::Result<std::shared_ptr<arrow::Table>> getTable();
17
18 std::shared_ptr<arrow::Schema> schema;
19
20 arrow::StringBuilder types;
21 arrow::StringBuilder data_owner_codes;
22 arrow::StringBuilder line_planning_numbers;
23 arrow::Date32Builder operating_days;
24 arrow::UInt32Builder journey_numbers;
25 arrow::UInt8Builder reinforcement_numbers;
26 arrow::TimestampBuilder timestamps{arrow::timestamp(arrow::TimeUnit::SECOND), arrow::default_memory_pool()};
27 arrow::StringBuilder sources;
28 arrow::Int16Builder punctualities;
29 arrow::StringBuilder user_stop_codes;
30 arrow::UInt16Builder passage_sequence_numbers;
31 arrow::UInt32Builder vehicle_numbers;
32 arrow::UInt32Builder block_codes;
33 arrow::StringBuilder wheelchair_accessibles;
34 arrow::UInt8Builder number_of_coaches;
35 arrow::Int32Builder rd_ys;
36 arrow::Int32Builder rd_xs;
37 arrow::UInt32Builder distance_since_last_user_stops;
38};
39
40[[nodiscard]]
41arrow::Status writeArrowRecordsAsParquetFile(arrow::RecordBatchReader &rbr, std::filesystem::path filename);
42
43[[nodiscard]]
44arrow::Status writeArrowTableAsParquetFile(const arrow::Table &table, std::filesystem::path filename);
45
46#endif // OEUF_LIBTMI8_KV6_PARQUET_HPP
diff --git a/lib/libtmi8/src/kv1_index.cpp b/lib/libtmi8/src/kv1_index.cpp
new file mode 100644
index 0000000..23e9596
--- /dev/null
+++ b/lib/libtmi8/src/kv1_index.cpp
@@ -0,0 +1,461 @@
1// vim:set sw=2 ts=2 sts et:
2
3#include <tmi8/kv1_index.hpp>
4
5Kv1Index::Kv1Index(Kv1Records *records) : records(records) {
6 organizational_units.reserve(records->organizational_units.size());
7 for (size_t i = 0; i < records->organizational_units.size(); i++) {
8 auto *it = &records->organizational_units[i];
9 organizational_units[it->key] = it;
10 }
11 higher_organizational_units.reserve(records->higher_organizational_units.size());
12 for (size_t i = 0; i < records->higher_organizational_units.size(); i++) {
13 auto *it = &records->higher_organizational_units[i];
14 higher_organizational_units[it->key] = it;
15 }
16 user_stop_points.reserve(records->user_stop_points.size());
17 for (size_t i = 0; i < records->user_stop_points.size(); i++) {
18 auto *it = &records->user_stop_points[i];
19 user_stop_points[it->key] = it;
20 }
21 user_stop_areas.reserve(records->user_stop_areas.size());
22 for (size_t i = 0; i < records->user_stop_areas.size(); i++) {
23 auto *it = &records->user_stop_areas[i];
24 user_stop_areas[it->key] = it;
25 }
26 timing_links.reserve(records->timing_links.size());
27 for (size_t i = 0; i < records->timing_links.size(); i++) {
28 auto *it = &records->timing_links[i];
29 timing_links[it->key] = it;
30 }
31 links.reserve(records->links.size());
32 for (size_t i = 0; i < records->links.size(); i++) {
33 auto *it = &records->links[i];
34 links[it->key] = it;
35 }
36 lines.reserve(records->lines.size());
37 for (size_t i = 0; i < records->lines.size(); i++) {
38 auto *it = &records->lines[i];
39 lines[it->key] = it;
40 }
41 destinations.reserve(records->destinations.size());
42 for (size_t i = 0; i < records->destinations.size(); i++) {
43 auto *it = &records->destinations[i];
44 destinations[it->key] = it;
45 }
46 journey_patterns.reserve(records->journey_patterns.size());
47 for (size_t i = 0; i < records->journey_patterns.size(); i++) {
48 auto *it = &records->journey_patterns[i];
49 journey_patterns[it->key] = it;
50 }
51 concession_financer_relations.reserve(records->concession_financer_relations.size());
52 for (size_t i = 0; i < records->concession_financer_relations.size(); i++) {
53 auto *it = &records->concession_financer_relations[i];
54 concession_financer_relations[it->key] = it;
55 }
56 concession_areas.reserve(records->concession_areas.size());
57 for (size_t i = 0; i < records->concession_areas.size(); i++) {
58 auto *it = &records->concession_areas[i];
59 concession_areas[it->key] = it;
60 }
61 financers.reserve(records->financers.size());
62 for (size_t i = 0; i < records->financers.size(); i++) {
63 auto *it = &records->financers[i];
64 financers[it->key] = it;
65 }
66 journey_pattern_timing_links.reserve(records->journey_pattern_timing_links.size());
67 for (size_t i = 0; i < records->journey_pattern_timing_links.size(); i++) {
68 auto *it = &records->journey_pattern_timing_links[i];
69 journey_pattern_timing_links[it->key] = it;
70 }
71 points.reserve(records->points.size());
72 for (size_t i = 0; i < records->points.size(); i++) {
73 auto *it = &records->points[i];
74 points[it->key] = it;
75 }
76 point_on_links.reserve(records->point_on_links.size());
77 for (size_t i = 0; i < records->point_on_links.size(); i++) {
78 auto *it = &records->point_on_links[i];
79 point_on_links[it->key] = it;
80 }
81 icons.reserve(records->icons.size());
82 for (size_t i = 0; i < records->icons.size(); i++) {
83 auto *it = &records->icons[i];
84 icons[it->key] = it;
85 }
86 notices.reserve(records->notices.size());
87 for (size_t i = 0; i < records->notices.size(); i++) {
88 auto *it = &records->notices[i];
89 notices[it->key] = it;
90 }
91 time_demand_groups.reserve(records->time_demand_groups.size());
92 for (size_t i = 0; i < records->time_demand_groups.size(); i++) {
93 auto *it = &records->time_demand_groups[i];
94 time_demand_groups[it->key] = it;
95 }
96 time_demand_group_run_times.reserve(records->time_demand_group_run_times.size());
97 for (size_t i = 0; i < records->time_demand_group_run_times.size(); i++) {
98 auto *it = &records->time_demand_group_run_times[i];
99 time_demand_group_run_times[it->key] = it;
100 }
101 period_groups.reserve(records->period_groups.size());
102 for (size_t i = 0; i < records->period_groups.size(); i++) {
103 auto *it = &records->period_groups[i];
104 period_groups[it->key] = it;
105 }
106 specific_days.reserve(records->specific_days.size());
107 for (size_t i = 0; i < records->specific_days.size(); i++) {
108 auto *it = &records->specific_days[i];
109 specific_days[it->key] = it;
110 }
111 timetable_versions.reserve(records->timetable_versions.size());
112 for (size_t i = 0; i < records->timetable_versions.size(); i++) {
113 auto *it = &records->timetable_versions[i];
114 timetable_versions[it->key] = it;
115 }
116 public_journeys.reserve(records->public_journeys.size());
117 for (size_t i = 0; i < records->public_journeys.size(); i++) {
118 auto *it = &records->public_journeys[i];
119 public_journeys[it->key] = it;
120 }
121 period_group_validities.reserve(records->period_group_validities.size());
122 for (size_t i = 0; i < records->period_group_validities.size(); i++) {
123 auto *it = &records->period_group_validities[i];
124 period_group_validities[it->key] = it;
125 }
126 exceptional_operating_days.reserve(records->exceptional_operating_days.size());
127 for (size_t i = 0; i < records->exceptional_operating_days.size(); i++) {
128 auto *it = &records->exceptional_operating_days[i];
129 exceptional_operating_days[it->key] = it;
130 }
131 schedule_versions.reserve(records->schedule_versions.size());
132 for (size_t i = 0; i < records->schedule_versions.size(); i++) {
133 auto *it = &records->schedule_versions[i];
134 schedule_versions[it->key] = it;
135 }
136 public_journey_passing_times.reserve(records->public_journey_passing_times.size());
137 for (size_t i = 0; i < records->public_journey_passing_times.size(); i++) {
138 auto *it = &records->public_journey_passing_times[i];
139 public_journey_passing_times[it->key] = it;
140 }
141 operating_days.reserve(records->operating_days.size());
142 for (size_t i = 0; i < records->operating_days.size(); i++) {
143 auto *it = &records->operating_days[i];
144 operating_days[it->key] = it;
145 }
146}
147
148size_t Kv1Index::size() const {
149 return organizational_units.size()
150 + higher_organizational_units.size()
151 + user_stop_points.size()
152 + user_stop_areas.size()
153 + timing_links.size()
154 + links.size()
155 + lines.size()
156 + destinations.size()
157 + journey_patterns.size()
158 + concession_financer_relations.size()
159 + concession_areas.size()
160 + financers.size()
161 + journey_pattern_timing_links.size()
162 + points.size()
163 + point_on_links.size()
164 + icons.size()
165 + notices.size()
166 + time_demand_groups.size()
167 + time_demand_group_run_times.size()
168 + period_groups.size()
169 + specific_days.size()
170 + timetable_versions.size()
171 + public_journeys.size()
172 + period_group_validities.size()
173 + exceptional_operating_days.size()
174 + schedule_versions.size()
175 + public_journey_passing_times.size()
176 + operating_days.size();
177}
178
179void kv1LinkRecords(Kv1Index &index) {
180 for (auto &orunorun : index.records->higher_organizational_units) {
181 Kv1OrganizationalUnit::Key orun_parent_key(
182 orunorun.key.data_owner_code,
183 orunorun.key.organizational_unit_code_parent);
184 Kv1OrganizationalUnit::Key orun_child_key(
185 orunorun.key.data_owner_code,
186 orunorun.key.organizational_unit_code_child);
187 orunorun.p_organizational_unit_parent = index.organizational_units[orun_parent_key];
188 orunorun.p_organizational_unit_child = index.organizational_units[orun_child_key];
189 }
190 for (auto &usrstop : index.records->user_stop_points) {
191 Kv1Point::Key point_key(
192 usrstop.key.data_owner_code,
193 usrstop.key.user_stop_code);
194 usrstop.p_point = index.points[point_key];
195 if (!usrstop.user_stop_area_code.empty()) {
196 Kv1UserStopArea::Key usrstar_key(
197 usrstop.key.data_owner_code,
198 usrstop.user_stop_area_code);
199 usrstop.p_user_stop_area = index.user_stop_areas[usrstar_key];
200 }
201 }
202 for (auto &tili : index.records->timing_links) {
203 Kv1UserStopPoint::Key usrstop_begin_key(
204 tili.key.data_owner_code,
205 tili.key.user_stop_code_begin);
206 Kv1UserStopPoint::Key usrstop_end_key(
207 tili.key.data_owner_code,
208 tili.key.user_stop_code_end);
209 tili.p_user_stop_begin = index.user_stop_points[usrstop_begin_key];
210 tili.p_user_stop_end = index.user_stop_points[usrstop_end_key];
211 }
212 for (auto &link : index.records->links) {
213 Kv1UserStopPoint::Key usrstop_begin_key(
214 link.key.data_owner_code,
215 link.key.user_stop_code_begin);
216 Kv1UserStopPoint::Key usrstop_end_key(
217 link.key.data_owner_code,
218 link.key.user_stop_code_end);
219 link.p_user_stop_begin = index.user_stop_points[usrstop_begin_key];
220 link.p_user_stop_end = index.user_stop_points[usrstop_end_key];
221 }
222 for (auto &line : index.records->lines) {
223 if (!line.line_icon)
224 continue;
225 Kv1Icon::Key icon_key(
226 line.key.data_owner_code,
227 *line.line_icon);
228 line.p_line_icon = index.icons[icon_key];
229 }
230 for (auto &jopa : index.records->journey_patterns) {
231 Kv1Line::Key line_key(
232 jopa.key.data_owner_code,
233 jopa.key.line_planning_number);
234 jopa.p_line = index.lines[line_key];
235 }
236 for (auto &confinrel : index.records->concession_financer_relations) {
237 Kv1ConcessionArea::Key conarea_key(
238 confinrel.key.data_owner_code,
239 confinrel.concession_area_code);
240 confinrel.p_concession_area = index.concession_areas[conarea_key];
241 if (!confinrel.financer_code.empty()) {
242 Kv1Financer::Key financer_key(
243 confinrel.key.data_owner_code,
244 confinrel.financer_code);
245 confinrel.p_financer = index.financers[financer_key];
246 }
247 }
248 for (auto &jopatili : index.records->journey_pattern_timing_links) {
249 Kv1Line::Key line_key(
250 jopatili.key.data_owner_code,
251 jopatili.key.line_planning_number);
252 Kv1JourneyPattern::Key jopa_key(
253 jopatili.key.data_owner_code,
254 jopatili.key.line_planning_number,
255 jopatili.key.journey_pattern_code);
256 Kv1UserStopPoint::Key usrstop_begin_key(
257 jopatili.key.data_owner_code,
258 jopatili.user_stop_code_begin);
259 Kv1UserStopPoint::Key usrstop_end_key(
260 jopatili.key.data_owner_code,
261 jopatili.user_stop_code_end);
262 Kv1ConcessionFinancerRelation::Key confinrel_key(
263 jopatili.key.data_owner_code,
264 jopatili.con_fin_rel_code);
265 Kv1Destination::Key dest_key(
266 jopatili.key.data_owner_code,
267 jopatili.dest_code);
268 jopatili.p_line = index.lines[line_key];
269 jopatili.p_journey_pattern = index.journey_patterns[jopa_key];
270 jopatili.p_user_stop_begin = index.user_stop_points[usrstop_begin_key];
271 jopatili.p_user_stop_end = index.user_stop_points[usrstop_end_key];
272 jopatili.p_con_fin_rel = index.concession_financer_relations[confinrel_key];
273 jopatili.p_dest = index.destinations[dest_key];
274 if (jopatili.line_dest_icon) {
275 Kv1Icon::Key icon_key{
276 jopatili.key.data_owner_code,
277 *jopatili.line_dest_icon,
278 };
279 jopatili.p_line_dest_icon = index.icons[icon_key];
280 }
281 }
282 for (auto &pool : index.records->point_on_links) {
283 Kv1UserStopPoint::Key usrstop_begin_key(
284 pool.key.data_owner_code,
285 pool.key.user_stop_code_begin);
286 Kv1UserStopPoint::Key usrstop_end_key(
287 pool.key.data_owner_code,
288 pool.key.user_stop_code_end);
289 Kv1Point::Key point_key(
290 pool.key.point_data_owner_code,
291 pool.key.point_code);
292 pool.p_user_stop_begin = index.user_stop_points[usrstop_begin_key];
293 pool.p_user_stop_end = index.user_stop_points[usrstop_end_key];
294 pool.p_point = index.points[point_key];
295 }
296 for (auto &ntcassgnm : index.records->notice_assignments) {
297 Kv1Notice::Key notice_key(
298 ntcassgnm.data_owner_code,
299 ntcassgnm.notice_code);
300 ntcassgnm.p_notice = index.notices[notice_key];
301 }
302 for (auto &timdemgrp : index.records->time_demand_groups) {
303 Kv1Line::Key line_key(
304 timdemgrp.key.data_owner_code,
305 timdemgrp.key.line_planning_number);
306 Kv1JourneyPattern::Key jopa_key(
307 timdemgrp.key.data_owner_code,
308 timdemgrp.key.line_planning_number,
309 timdemgrp.key.journey_pattern_code);
310 timdemgrp.p_line = index.lines[line_key];
311 timdemgrp.p_journey_pattern = index.journey_patterns[jopa_key];
312 }
313 for (auto &timdemrnt : index.records->time_demand_group_run_times) {
314 Kv1Line::Key line_key(
315 timdemrnt.key.data_owner_code,
316 timdemrnt.key.line_planning_number);
317 Kv1JourneyPattern::Key jopa_key(
318 timdemrnt.key.data_owner_code,
319 timdemrnt.key.line_planning_number,
320 timdemrnt.key.journey_pattern_code);
321 Kv1TimeDemandGroup::Key timdemgrp_key(
322 timdemrnt.key.data_owner_code,
323 timdemrnt.key.line_planning_number,
324 timdemrnt.key.journey_pattern_code,
325 timdemrnt.key.time_demand_group_code);
326 Kv1UserStopPoint::Key usrstop_begin_key(
327 timdemrnt.key.data_owner_code,
328 timdemrnt.user_stop_code_begin);
329 Kv1UserStopPoint::Key usrstop_end_key(
330 timdemrnt.key.data_owner_code,
331 timdemrnt.user_stop_code_end);
332 Kv1JourneyPatternTimingLink::Key jopatili_key(
333 timdemrnt.key.data_owner_code,
334 timdemrnt.key.line_planning_number,
335 timdemrnt.key.journey_pattern_code,
336 timdemrnt.key.timing_link_order);
337 timdemrnt.p_line = index.lines[line_key];
338 timdemrnt.p_user_stop_end = index.user_stop_points[usrstop_end_key];
339 timdemrnt.p_user_stop_begin = index.user_stop_points[usrstop_begin_key];
340 timdemrnt.p_journey_pattern = index.journey_patterns[jopa_key];
341 timdemrnt.p_time_demand_group = index.time_demand_groups[timdemgrp_key];
342 timdemrnt.p_journey_pattern_timing_link = index.journey_pattern_timing_links[jopatili_key];
343 }
344 for (auto &tive : index.records->timetable_versions) {
345 Kv1OrganizationalUnit::Key orun_key(
346 tive.key.data_owner_code,
347 tive.key.organizational_unit_code);
348 Kv1PeriodGroup::Key pegr_key(
349 tive.key.data_owner_code,
350 tive.key.period_group_code);
351 Kv1SpecificDay::Key specday_key(
352 tive.key.data_owner_code,
353 tive.key.specific_day_code);
354 tive.p_organizational_unit = index.organizational_units[orun_key];
355 tive.p_period_group = index.period_groups[pegr_key];
356 tive.p_specific_day = index.specific_days[specday_key];
357 }
358 for (auto &pujo : index.records->public_journeys) {
359 Kv1TimetableVersion::Key tive_key(
360 pujo.key.data_owner_code,
361 pujo.key.organizational_unit_code,
362 pujo.key.timetable_version_code,
363 pujo.key.period_group_code,
364 pujo.key.specific_day_code);
365 Kv1OrganizationalUnit::Key orun_key(
366 pujo.key.data_owner_code,
367 pujo.key.organizational_unit_code);
368 Kv1PeriodGroup::Key pegr_key(
369 pujo.key.data_owner_code,
370 pujo.key.period_group_code);
371 Kv1SpecificDay::Key specday_key(
372 pujo.key.data_owner_code,
373 pujo.key.specific_day_code);
374 Kv1Line::Key line_key(
375 pujo.key.data_owner_code,
376 pujo.key.line_planning_number);
377 Kv1TimeDemandGroup::Key timdemgrp_key(
378 pujo.key.data_owner_code,
379 pujo.key.line_planning_number,
380 pujo.journey_pattern_code,
381 pujo.time_demand_group_code);
382 Kv1JourneyPattern::Key jopa_key(
383 pujo.key.data_owner_code,
384 pujo.key.line_planning_number,
385 pujo.journey_pattern_code);
386 pujo.p_timetable_version = index.timetable_versions[tive_key];
387 pujo.p_organizational_unit = index.organizational_units[orun_key];
388 pujo.p_period_group = index.period_groups[pegr_key];
389 pujo.p_specific_day = index.specific_days[specday_key];
390 pujo.p_line = index.lines[line_key];
391 pujo.p_time_demand_group = index.time_demand_groups[timdemgrp_key];
392 pujo.p_journey_pattern = index.journey_patterns[jopa_key];
393 }
394 for (auto &pegrval : index.records->period_group_validities) {
395 Kv1OrganizationalUnit::Key orun_key(
396 pegrval.key.data_owner_code,
397 pegrval.key.organizational_unit_code);
398 Kv1PeriodGroup::Key pegr_key(
399 pegrval.key.data_owner_code,
400 pegrval.key.period_group_code);
401 pegrval.p_organizational_unit = index.organizational_units[orun_key];
402 pegrval.p_period_group = index.period_groups[pegr_key];
403 }
404 for (auto &excopday : index.records->exceptional_operating_days) {
405 Kv1OrganizationalUnit::Key orun_key(
406 excopday.key.data_owner_code,
407 excopday.key.organizational_unit_code);
408 Kv1SpecificDay::Key specday_key(
409 excopday.key.data_owner_code,
410 excopday.specific_day_code);
411 Kv1PeriodGroup::Key pegr_key(
412 excopday.key.data_owner_code,
413 excopday.period_group_code);
414 excopday.p_organizational_unit = index.organizational_units[orun_key];
415 excopday.p_specific_day = index.specific_days[specday_key];
416 excopday.p_period_group = index.period_groups[pegr_key];
417 }
418 for (auto &schedvers : index.records->schedule_versions) {
419 Kv1OrganizationalUnit::Key orun_key(
420 schedvers.key.data_owner_code,
421 schedvers.key.organizational_unit_code);
422 schedvers.p_organizational_unit = index.organizational_units[orun_key];
423 }
424 for (auto &pujopass : index.records->public_journey_passing_times) {
425 Kv1OrganizationalUnit::Key orun_key(
426 pujopass.key.data_owner_code,
427 pujopass.key.organizational_unit_code);
428 Kv1ScheduleVersion::Key schedvers_key(
429 pujopass.key.data_owner_code,
430 pujopass.key.organizational_unit_code,
431 pujopass.key.schedule_code,
432 pujopass.key.schedule_type_code);
433 Kv1Line::Key line_key(
434 pujopass.key.data_owner_code,
435 pujopass.key.line_planning_number);
436 Kv1JourneyPattern::Key jopa_key(
437 pujopass.key.data_owner_code,
438 pujopass.key.line_planning_number,
439 pujopass.journey_pattern_code);
440 Kv1UserStopPoint::Key usrstop_key(
441 pujopass.key.data_owner_code,
442 pujopass.user_stop_code);
443 pujopass.p_organizational_unit = index.organizational_units[orun_key];
444 pujopass.p_schedule_version = index.schedule_versions[schedvers_key];
445 pujopass.p_line = index.lines[line_key];
446 pujopass.p_journey_pattern = index.journey_patterns[jopa_key];
447 pujopass.p_user_stop = index.user_stop_points[usrstop_key];
448 }
449 for (auto &operday : index.records->operating_days) {
450 Kv1OrganizationalUnit::Key orun_key(
451 operday.key.data_owner_code,
452 operday.key.organizational_unit_code);
453 Kv1ScheduleVersion::Key schedvers_key(
454 operday.key.data_owner_code,
455 operday.key.organizational_unit_code,
456 operday.key.schedule_code,
457 operday.key.schedule_type_code);
458 operday.p_organizational_unit = index.organizational_units[orun_key];
459 operday.p_schedule_version = index.schedule_versions[schedvers_key];
460 }
461}
diff --git a/lib/libtmi8/src/kv1_lexer.cpp b/lib/libtmi8/src/kv1_lexer.cpp
new file mode 100644
index 0000000..028127b
--- /dev/null
+++ b/lib/libtmi8/src/kv1_lexer.cpp
@@ -0,0 +1,152 @@
1// vim:set sw=2 ts=2 sts et:
2
3#include <tmi8/kv1_lexer.hpp>
4
5Kv1Lexer::Kv1Lexer(std::string_view input)
6 : input(input), slice(input)
7{}
8
9// Does not eat newline character.
10void Kv1Lexer::eatRestOfLine() {
11 size_t end = slice.size();
12 for (size_t i = 0; i < slice.size(); i++) {
13 if (slice[i] == '\r' || slice[i] == '\n') {
14 end = i;
15 break;
16 }
17 }
18 slice = slice.substr(end);
19}
20
21void Kv1Lexer::lexOptionalHeader() {
22 if (slice.starts_with('[')) eatRestOfLine();
23}
24
25void Kv1Lexer::lexOptionalComment() {
26 if (slice.starts_with(';')) eatRestOfLine();
27}
28
29inline bool Kv1Lexer::isWhitespace(int c) {
30 return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '\v';
31}
32
33void Kv1Lexer::readQuotedColumn() {
34 Kv1Token token{ .type = KV1_TOKEN_CELL };
35
36 if (slice.size() == 0 || slice[0] != '"') {
37 errors.push_back("(internal error) readQuotedColumn: slice[0] != '\"'");
38 return;
39 }
40 slice = slice.substr(1);
41 while (true) {
42 size_t quote = slice.find('"');
43 if (quote == std::string_view::npos) {
44 errors.push_back("readQuotedColumn: no matching closing quote found");
45 return;
46 }
47 if (quote+1 == slice.size() || slice[quote + 1] != '"') {
48 token.data.append(slice.substr(0, quote));
49 break;
50 }
51 token.data.append(slice.substr(0, quote + 1));
52 slice = slice.substr(quote + 2);
53 }
54
55 size_t end = slice.size();
56 for (size_t i = 0; i < slice.size(); i++) {
57 if (slice[i] == '|' || slice[i] == '\r' || slice[i] == '\n') {
58 end = i;
59 break;
60 }
61 if (!isWhitespace(slice[i])) {
62 errors.push_back("readQuotedColumn: encountered non-whitespace character after closing quote");
63 return;
64 }
65 }
66 if (end != std::string_view::npos) slice = slice.substr(end);
67 else slice = slice.substr(slice.size());
68
69 tokens.push_back(std::move(token));
70}
71
72void Kv1Lexer::readUnquotedColumn() {
73 size_t end = slice.size();
74 size_t content_end = 0;
75 for (size_t i = 0; i < slice.size(); i++) {
76 if (slice[i] == '|' || slice[i] == '\r' || slice[i] == '\n') {
77 end = i;
78 break;
79 } else if (!isWhitespace(slice[i])) {
80 content_end = i + 1;
81 }
82 }
83 tokens.emplace_back(KV1_TOKEN_CELL, std::string(slice.substr(0, content_end)));
84 if (end != std::string_view::npos) slice = slice.substr(end);
85 else slice = slice.substr(slice.size());
86}
87
88void Kv1Lexer::lexRow() {
89 size_t cols = 0;
90 while (slice.size() > 0 && slice[0] != '\r' && slice[0] != '\n') {
91 if (slice[0] == '"') readQuotedColumn();
92 else readUnquotedColumn();
93 if (!errors.empty()) return;
94 cols++;
95 if (slice.size() != 0) {
96 if (slice[0] == '|') {
97 slice = slice.substr(1);
98 // A newline/eof right after pipe? That means an empty field at the end
99 // of the record, we also want to emit that as a token.
100 if (slice.size() == 0 || slice[0] == '\r' || slice[0] == '\n') {
101 tokens.push_back({ .type = KV1_TOKEN_CELL });
102 }
103 } else if (slice[0] == '\r') {
104 if (slice.size() > 1 && slice[1] == '\n') slice = slice.substr(2);
105 else slice = slice.substr(1);
106 break;
107 } else if (slice[0] == '\n') {
108 slice = slice.substr(1);
109 break;
110 } else {
111 errors.push_back("lexRow: expected CR, LF or |");
112 return;
113 }
114 }
115 }
116 tokens.push_back({ .type = KV1_TOKEN_ROW_END });
117}
118
119// Returns true when a line ending was consumed.
120bool Kv1Lexer::eatWhitespace() {
121 for (size_t i = 0; i < slice.size(); i++) {
122 if (slice[i] == '\r') {
123 slice = slice.substr(i + 1);
124 if (slice.size() > 1 && slice[i + 1] == '\n')
125 slice = slice.substr(i + 2);
126 return true;
127 }
128 if (slice[i] == '\n') {
129 slice = slice.substr(i + 1);
130 return true;
131 }
132
133 if (slice[i] != ' ' && slice[i] != '\f' && slice[i] != '\t' && slice[i] != '\v') {
134 slice = slice.substr(i);
135 return false;
136 }
137 }
138 return false;
139}
140
141void Kv1Lexer::lex() {
142 lexOptionalHeader();
143 eatWhitespace();
144
145 while (errors.empty() && !slice.empty()) {
146 lexOptionalComment();
147 bool newline = eatWhitespace();
148 if (newline) continue;
149 // We are now either (1) at the end of the file or (2) at the start of some column data
150 if (errors.empty()) lexRow();
151 }
152}
diff --git a/lib/libtmi8/src/kv1_parser.cpp b/lib/libtmi8/src/kv1_parser.cpp
new file mode 100644
index 0000000..ac0c6bf
--- /dev/null
+++ b/lib/libtmi8/src/kv1_parser.cpp
@@ -0,0 +1,1258 @@
1// vim:set sw=2 ts=2 sts et:
2
3#include <tmi8/kv1_parser.hpp>
4
5using rune = uint32_t;
6
7static size_t decodeUtf8Cp(std::string_view s, rune *dest = nullptr) {
8 rune res = 0xFFFD;
9 size_t length = 1;
10
11 if (s.size() == 0)
12 return 0;
13 const uint8_t *b = reinterpret_cast<const uint8_t *>(s.data());
14 if (!(b[0] & 0x80))
15 res = static_cast<rune>(b[0]);
16 else if ((b[0] & 0xE0) == 0xC0) {
17 length = 2;
18 if (s.size() >= 2 && (b[1] & 0xC0) == 0x80) {
19 res = static_cast<rune>(b[0] & ~0xC0) << 6;
20 res |= static_cast<rune>(b[1] & ~0x80);
21 }
22 } else if ((b[0] & 0xF0) == 0xE0) {
23 length = 3;
24 if (s.size() >= 3 && (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80) {
25 res = static_cast<rune>(b[0] & ~0xE0) << 12;
26 res |= static_cast<rune>(b[1] & ~0x80) << 6;
27 res |= static_cast<rune>(b[2] & ~0x80);
28 }
29 } else if (b[0] == 0xF0) {
30 length = 4;
31 if (s.size() >= 4 && (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 && (b[3] & 0xC0) == 0x80) {
32 res = static_cast<rune>(b[0] & ~0xF0) << 18;
33 res |= static_cast<rune>(b[1] & ~0x80) << 12;
34 res |= static_cast<rune>(b[2] & ~0x80) << 6;
35 res |= static_cast<rune>(b[3] & ~0x80);
36 }
37 }
38
39 if (dest)
40 *dest = res;
41 return length;
42}
43
44// Counts the number of codepoints in a valid UTF-8 string. Returns SIZE_MAX if
45// the string contains invalid UTF-8 codepoints.
46static size_t stringViewLengthUtf8(std::string_view sv) {
47 size_t codepoints = 0;
48 while (sv.size() > 0) {
49 size_t codepoint_size = decodeUtf8Cp(sv);
50 if (codepoint_size == 0) return SIZE_MAX;
51 codepoints++;
52 sv = sv.substr(codepoint_size);
53 }
54 return codepoints;
55}
56
57Kv1Parser::Kv1Parser(std::vector<Kv1Token> tokens, Kv1Records &parse_into)
58 : tokens(std::move(tokens)),
59 records(parse_into)
60{}
61
62bool Kv1Parser::atEnd() const {
63 return pos >= tokens.size();
64}
65
66void Kv1Parser::eatRowEnds() {
67 while (!atEnd() && tokens[pos].type == KV1_TOKEN_ROW_END) pos++;
68}
69
70const Kv1Token *Kv1Parser::cur() const {
71 if (atEnd()) return nullptr;
72 return &tokens[pos];
73}
74
75const std::string *Kv1Parser::eatCell(std::string_view parsing_what) {
76 const Kv1Token *tok = cur();
77 if (!tok) {
78 record_errors.push_back(std::format("Expected cell but got end of file when parsing {}", parsing_what));
79 return nullptr;
80 }
81 if (tok->type == KV1_TOKEN_ROW_END) {
82 record_errors.push_back(std::format("Expected cell but got end of row when parsing {}", parsing_what));
83 return nullptr;
84 }
85 pos++;
86 return &tok->data;
87}
88
89void Kv1Parser::requireString(std::string_view field, bool mandatory, size_t max_length, std::string_view value) {
90 if (value.empty() && mandatory) {
91 record_errors.push_back(std::format("{} has length zero but is required", field));
92 return;
93 }
94 size_t codepoints = stringViewLengthUtf8(value);
95 if (codepoints == SIZE_MAX) {
96 global_errors.push_back(std::format("{} contains invalid UTF-8 code points", field));
97 return;
98 }
99 if (codepoints > max_length) {
100 record_errors.push_back(std::format("{} has length ({}) that is greater than maximum length ({})",
101 field, value.size(), max_length));
102 }
103}
104
105static inline std::optional<bool> parseBoolean(std::string_view src) {
106 if (src == "1") return true;
107 if (src == "0") return false;
108 if (src == "true") return true;
109 if (src == "false") return false;
110 return std::nullopt;
111}
112
113std::optional<bool> Kv1Parser::requireBoolean(std::string_view field, bool mandatory, std::string_view value) {
114 if (value.empty()) {
115 if (mandatory)
116 record_errors.push_back(std::format("{} is required, but has no value", field));
117 return std::nullopt;
118 }
119 auto parsed = parseBoolean(value);
120 if (!parsed.has_value())
121 record_errors.push_back(std::format("{} should have value \"1\", \"0\", \"true\" or \"false\"", field));
122 return parsed;
123}
124
125static inline size_t countDigits(long x) {
126 size_t digits = 0;
127 while (x != 0) { digits++; x /= 10; }
128 return digits;
129}
130
131std::optional<double> Kv1Parser::requireNumber(std::string_view field, bool mandatory, size_t max_digits, std::string_view value) {
132 if (value.empty()) {
133 if (mandatory)
134 record_errors.push_back(std::format("{} has no value but is required", field));
135 return std::nullopt;
136 }
137
138 double parsed;
139 auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), parsed, std::chars_format::fixed);
140 if (ec != std::errc()) {
141 record_errors.push_back(std::format("{} has a bad value that cannot be parsed as a number", field));
142 return std::nullopt;
143 }
144 if (ptr != value.data() + value.size()) {
145 record_errors.push_back(std::format("{} contains characters that were not parsed as a number", field));
146 return std::nullopt;
147 }
148
149 size_t digits = countDigits(static_cast<long>(parsed));
150 if (digits > max_digits) {
151 record_errors.push_back(std::format("{} contains more digits (in the integral part) ({}) than allowed ({})",
152 field, digits, max_digits));
153 return std::nullopt;
154 }
155
156 return parsed;
157}
158
159static inline bool isHexDigit(char c) {
160 return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F');
161}
162
163static inline uint8_t fromHex(char c) {
164 if (c >= '0' && c <= '9') return static_cast<uint8_t>(c - '0');
165 else if (c >= 'A' && c <= 'F') return static_cast<uint8_t>(c - 'A' + 10);
166 return 0;
167}
168
169static std::optional<RgbColor> parseRgbColor(std::string_view src) {
170 bool valid = src.size() == 6
171 && isHexDigit(src[0]) && isHexDigit(src[1])
172 && isHexDigit(src[2]) && isHexDigit(src[3])
173 && isHexDigit(src[4]) && isHexDigit(src[5]);
174 if (!valid) return std::nullopt;
175 uint8_t r = static_cast<uint8_t>(fromHex(src[0]) << 4) + fromHex(src[1]);
176 uint8_t g = static_cast<uint8_t>(fromHex(src[2]) << 4) + fromHex(src[3]);
177 uint8_t b = static_cast<uint8_t>(fromHex(src[4]) << 4) + fromHex(src[5]);
178 return RgbColor{ r, g, b };
179}
180
181std::optional<RgbColor> Kv1Parser::requireRgbColor(std::string_view field, bool mandatory, std::string_view value) {
182 if (value.empty()) {
183 if (mandatory)
184 record_errors.push_back(std::format("{} is required, but has no value", field));
185 return std::nullopt;
186 }
187 auto parsed = parseRgbColor(value);
188 if (!parsed.has_value())
189 record_errors.push_back(std::format("{} should be an RGB color, i.e. a sequence of six hexadecimally represented nibbles", field));
190 return parsed;
191}
192
193std::optional<double> Kv1Parser::requireRdCoord(std::string_view field, bool mandatory, size_t min_digits, std::string_view value) {
194 if (value.empty()) {
195 if (mandatory)
196 record_errors.push_back(std::format("{} is required, but has no value", field));
197 return std::nullopt;
198 }
199 if (value.size() > 15) {
200 record_errors.push_back(std::format("{} may not have more than 15 characters", field));
201 return std::nullopt;
202 }
203
204 double parsed;
205 auto [ptr, ec] = std::from_chars(value.data(), value.data() + value.size(), parsed, std::chars_format::fixed);
206 if (ec != std::errc()) {
207 record_errors.push_back(std::format("{} has a bad value that cannot be parsed as a number", field));
208 return std::nullopt;
209 }
210 if (ptr != value.data() + value.size()) {
211 record_errors.push_back(std::format("{} contains characters that were not parsed as a number", field));
212 return std::nullopt;
213 }
214
215 size_t digits = countDigits(static_cast<long>(parsed));
216 if (digits < min_digits) {
217 record_errors.push_back(std::format("{} contains less digits (in the integral part) ({}) than required ({}) [value: {}]",
218 field, digits, min_digits, value));
219 return std::nullopt;
220 }
221
222 return parsed;
223}
224
225std::string Kv1Parser::eatString(std::string_view field, bool mandatory, size_t max_length) {
226 auto value = eatCell(field);
227 if (!record_errors.empty()) return {};
228 requireString(field, mandatory, max_length, *value);
229 return std::move(*value);
230}
231
232std::optional<bool> Kv1Parser::eatBoolean(std::string_view field, bool mandatory) {
233 auto value = eatCell(field);
234 if (!record_errors.empty()) return {};
235 return requireBoolean(field, mandatory, *value);
236}
237
238std::optional<double> Kv1Parser::eatNumber(std::string_view field, bool mandatory, size_t max_digits) {
239 auto value = eatCell(field);
240 if (!record_errors.empty()) return {};
241 return requireNumber(field, mandatory, max_digits, *value);
242}
243
244std::optional<RgbColor> Kv1Parser::eatRgbColor(std::string_view field, bool mandatory) {
245 auto value = eatCell(field);
246 if (!record_errors.empty()) return {};
247 return requireRgbColor(field, mandatory, *value);
248}
249
250std::optional<double> Kv1Parser::eatRdCoord(std::string_view field, bool mandatory, size_t min_digits) {
251 auto value = eatCell(field);
252 if (!record_errors.empty()) return {};
253 return requireRdCoord(field, mandatory, min_digits, *value);
254}
255
256std::string Kv1Parser::parseHeader() {
257 auto record_type = eatString("<header>.Recordtype", true, 10);
258 auto version_number = eatString("<header>.VersionNumber", true, 2);
259 auto implicit_explicit = eatString("<header>.Implicit/Explicit", true, 1);
260 if (!record_errors.empty()) return {};
261
262 if (version_number != "1") {
263 record_errors.push_back("<header>.VersionNumber should be 1");
264 return "";
265 }
266 if (implicit_explicit != "I") {
267 record_errors.push_back("<header>.Implicit/Explicit should be 'I'");
268 return "";
269 }
270
271 return record_type;
272}
273
274void Kv1Parser::eatRestOfRow() {
275 while (!atEnd() && cur()->type != KV1_TOKEN_ROW_END) pos++;
276}
277
278void Kv1Parser::parse() {
279 while (!atEnd()) {
280 eatRowEnds();
281 if (atEnd()) return;
282
283 std::string record_type = parseHeader();
284 if (!record_errors.empty()) break;
285 if (!type_parsers.contains(record_type)) {
286 warns.push_back(std::format("Recordtype ({}) is bad or names a record type that this program cannot process",
287 record_type));
288 eatRestOfRow();
289 continue;
290 }
291
292 ParseFunc parseType = Kv1Parser::type_parsers.at(record_type);
293 (this->*parseType)();
294 if (cur() && cur()->type != KV1_TOKEN_ROW_END) {
295 record_errors.push_back(std::format("Parser function for Recordtype ({}) did not eat all record fields",
296 record_type));
297 eatRestOfRow();
298 }
299 if (!record_errors.empty()) {
300 global_errors.insert(global_errors.end(), record_errors.begin(), record_errors.end());
301 record_errors.clear();
302 }
303 }
304}
305
306void Kv1Parser::parseOrganizationalUnit() {
307 auto data_owner_code = eatString("ORUN.DataOwnerCode", true, 10);
308 auto organizational_unit_code = eatString("ORUN.OrganizationalUnitCode", true, 10);
309 auto name = eatString("ORUN.Name", true, 50);
310 auto organizational_unit_type = eatString("ORUN.OrganizationalUnitType", true, 10);
311 auto description = eatString("ORUN.Description", false, 255);
312 if (!record_errors.empty()) return;
313
314 records.organizational_units.emplace_back(
315 Kv1OrganizationalUnit::Key(
316 data_owner_code,
317 organizational_unit_code),
318 name,
319 organizational_unit_type,
320 description);
321}
322
323static inline bool isDigit(char c) {
324 return c >= '0' && c <= '9';
325}
326
327// Parse a string of the format YYYY-MM-DD.
328static std::optional<std::chrono::year_month_day> parseYyyymmdd(std::string_view src) {
329 bool valid = src.size() == 10
330 && isDigit(src[0]) && isDigit(src[1])
331 && isDigit(src[2]) && isDigit(src[3]) && src[4] == '-'
332 && isDigit(src[5]) && isDigit(src[6]) && src[7] == '-'
333 && isDigit(src[8]) && isDigit(src[9]);
334 if (!valid) return std::nullopt;
335 int year = (src[0] - '0') * 1000 + (src[1] - '0') * 100 + (src[2] - '0') * 10 + src[3] - '0';
336 int month = (src[5] - '0') * 10 + src[6] - '0';
337 int day = (src[8] - '0') * 10 + src[9] - '0';
338 return std::chrono::year(year) / std::chrono::month(month) / std::chrono::day(day);
339}
340
341// Parse a string of the format HH:MM:SS.
342static std::optional<std::chrono::hh_mm_ss<std::chrono::seconds>> parseHhmmss(std::string_view src) {
343 bool valid = src.size() == 8
344 && isDigit(src[0]) && isDigit(src[1]) && src[2] == ':'
345 && isDigit(src[3]) && isDigit(src[4]) && src[5] == ':'
346 && isDigit(src[6]) && isDigit(src[7]);
347 if (!valid) return std::nullopt;
348 int hh = (src[0] - '0') * 10 + src[1] - '0';
349 int mm = (src[3] - '0') * 10 + src[4] - '0';
350 int ss = (src[6] - '0') * 10 + src[7] - '0';
351 // The check for the hour not being greater than 32 comes from the fact the
352 // specification explicitly allows hours greater than 23, noting that the
353 // period 24:00-32:00 is equivalent to 00:00-08:00 in the next day, for
354 // exploitation of two days.
355 if (hh > 32 || mm > 59 || ss > 59) return std::nullopt;
356 return std::chrono::hh_mm_ss(std::chrono::hours(hh) + std::chrono::minutes(mm) + std::chrono::seconds(ss));
357}
358
359static std::optional<std::chrono::sys_seconds> parseDateTime(std::string_view src, const std::chrono::time_zone *amsterdam, std::string_view *error = nullptr) {
360#define ERROR(err) do { if (error) *error = err; return std::nullopt; } while (0)
361 if (src.size() > 23) ERROR("timestamp string is too big");
362 if (src.size() < 17) ERROR("timestamp string is too small");
363
364 bool valid_year = isDigit(src[0]) && isDigit(src[1]) && isDigit(src[2]) && isDigit(src[3]);
365 if (!valid_year) ERROR("year has bad format");
366
367 size_t month_off = src[4] == '-' ? 5 : 4;
368 size_t day_off = src[month_off + 2] == '-' ? month_off + 3 : month_off + 2;
369 size_t time_off = day_off + 2;
370 if (src[time_off] != 'T' && src[time_off] != ' ')
371 ERROR("missing date/time separator");
372 size_t tzd_off = time_off + 9;
373 // For clarity, TZD stands for Time Zone Designator. It often takes the form
374 // of Z (Zulu, UTC+00:00) or as an offset from UTC in hours and minutes,
375 // formatted as +|-HH:MM (e.g. +01:00, -12:00).
376
377 if (time_off + 8 >= src.size()) ERROR("bad format, not enough space for hh:mm:ss");
378
379 int year = (src[0] - '0') * 1000 + (src[1] - '0') * 100 + (src[2] - '0') * 10 + src[3] - '0';
380 int month = (src[month_off] - '0') * 10 + src[month_off + 1] - '0';
381 int day = (src[day_off] - '0') * 10 + src[day_off + 1] - '0';
382 int hour = (src[time_off + 1] - '0') * 10 + src[time_off + 2] - '0';
383 int minute = (src[time_off + 4] - '0') * 10 + src[time_off + 5] - '0';
384 int second = (src[time_off + 7] - '0') * 10 + src[time_off + 8] - '0';
385
386 auto date = std::chrono::year(year) / std::chrono::month(month) / std::chrono::day(day);
387 auto time = std::chrono::hours(hour) + std::chrono::minutes(minute) + std::chrono::seconds(second);
388
389 std::chrono::sys_seconds unix_start_of_day;
390 if (tzd_off < src.size()) {
391 unix_start_of_day = std::chrono::sys_days(date);
392 } else {
393 auto local_days = std::chrono::local_days(date);
394 std::chrono::zoned_seconds zoned_start_of_day = std::chrono::zoned_time(amsterdam, local_days);
395 unix_start_of_day = std::chrono::sys_seconds(zoned_start_of_day);
396 }
397
398 std::chrono::minutes offset(0);
399 if (tzd_off + 1 == src.size() && src[tzd_off] != 'Z') {
400 ERROR("bad TZD (missing Zulu indicator)");
401 } else if (tzd_off + 6 == src.size()) {
402 bool valid_tzd = (src[tzd_off] == '+' || src[tzd_off] == '-')
403 && isDigit(src[tzd_off + 1]) && isDigit(src[tzd_off + 2]) && src[tzd_off + 3] == ':'
404 && isDigit(src[tzd_off + 4]) && isDigit(src[tzd_off + 5]);
405 if (!valid_tzd) ERROR("bad offset TZD format (expected +|-hh:mm)");
406 int sign = src[tzd_off] == '-' ? -1 : 1;
407 int tzd_hh = (src[tzd_off + 1] - '0') * 10 + src[tzd_off + 2] - '0';
408 int tzd_mm = (src[tzd_off + 3] - '0') * 10 + src[tzd_off + 4] - '0';
409 offset = sign * std::chrono::minutes(tzd_hh * 60 + tzd_mm);
410 } else if (tzd_off < src.size()) {
411 // There is a TZD but we literally have no clue how to parse it :/
412 ERROR("cannot parse TZD of unexpected length");
413 }
414
415 return unix_start_of_day + time - offset;
416#undef ERROR
417}
418
419void Kv1Parser::parseHigherOrganizationalUnit() {
420 auto data_owner_code = eatString("ORUNORUN.DataOwnerCode", true, 10);
421 auto organizational_unit_code_parent = eatString("ORUNORUN.OrganizationalUnitCodeParent", true, 10);
422 auto organizational_unit_code_child = eatString("ORUNORUN.OrganizationalUnitCodeChild", true, 10);
423 auto valid_from_raw = eatString("ORUNORUN.ValidFrom", true, 10);
424 if (!record_errors.empty()) return;
425
426 auto valid_from = parseYyyymmdd(valid_from_raw);
427 if (!valid_from) {
428 record_errors.push_back("ORUNORUN.ValidFrom has invalid format, should be YYYY-MM-DD");
429 return;
430 }
431
432 records.higher_organizational_units.emplace_back(
433 Kv1HigherOrganizationalUnit::Key(
434 data_owner_code,
435 organizational_unit_code_parent,
436 organizational_unit_code_child,
437 *valid_from));
438}
439
440void Kv1Parser::parseUserStopPoint() {
441 auto data_owner_code = eatString ("USRSTOP.DataOwnerCode", true, 10);
442 auto user_stop_code = eatString ("USRSTOP.UserStopCode", true, 10);
443 auto timing_point_code = eatString ("USRSTOP.TimingPointCode", false, 10);
444 auto get_in = eatBoolean("USRSTOP.GetIn", true );
445 auto get_out = eatBoolean("USRSTOP.GetOut", true );
446 eatCell ("USRSTOP.<deprecated field #1>" );
447 auto name = eatString ("USRSTOP.Name", true, 50);
448 auto town = eatString ("USRSTOP.Town", true, 50);
449 auto user_stop_area_code = eatString ("USRSTOP.UserStopAreaCode", false, 10);
450 auto stop_side_code = eatString ("USRSTOP.StopSideCode", true, 10);
451 eatCell ("USRSTOP.<deprecated field #2>" );
452 eatCell ("USRSTOP.<deprecated field #3>" );
453 auto minimal_stop_time = eatNumber ("USRSTOP.MinimalStopTime", true, 5);
454 auto stop_side_length = eatNumber ("USRSTOP.StopSideLength", false, 3);
455 auto description = eatString ("USRSTOP.Description", false, 255);
456 auto user_stop_type = eatString ("USRSTOP.UserStopType", true, 10);
457 auto quay_code = eatString ("USRSTOP.QuayCode", false, 30);
458 if (!record_errors.empty()) return;
459
460 records.user_stop_points.emplace_back(
461 Kv1UserStopPoint::Key(
462 data_owner_code,
463 user_stop_code),
464 timing_point_code,
465 *get_in,
466 *get_out,
467 name,
468 town,
469 user_stop_area_code,
470 stop_side_code,
471 *minimal_stop_time,
472 stop_side_length,
473 description,
474 user_stop_type,
475 quay_code);
476}
477
478void Kv1Parser::parseUserStopArea() {
479 auto data_owner_code = eatString("USRSTAR.DataOwnerCode", true, 10);
480 auto user_stop_area_code = eatString("USRSTAR.UserStopAreaCode", true, 10);
481 auto name = eatString("USRSTAR.Name", true, 50);
482 auto town = eatString("USRSTAR.Town", true, 50);
483 eatCell ("USRSTAR.<deprecated field #1>" );
484 eatCell ("USRSTAR.<deprecated field #2>" );
485 auto description = eatString("USRSTAR.Description", false, 255);
486 if (!record_errors.empty()) return;
487
488 records.user_stop_areas.emplace_back(
489 Kv1UserStopArea::Key(
490 data_owner_code,
491 user_stop_area_code),
492 name,
493 town,
494 description);
495}
496
497void Kv1Parser::parseTimingLink() {
498 auto data_owner_code = eatString("TILI.DataOwnerCode", true, 10);
499 auto user_stop_code_begin = eatString("TILI.UserStopCodeBegin", true, 10);
500 auto user_stop_code_end = eatString("TILI.UserStopCodeEnd", true, 10);
501 auto minimal_drive_time = eatNumber("TILI.MinimalDriveTime", false, 5);
502 auto description = eatString("TILI.Description", false, 255);
503 if (!record_errors.empty()) return;
504
505 records.timing_links.emplace_back(
506 Kv1TimingLink::Key(
507 data_owner_code,
508 user_stop_code_begin,
509 user_stop_code_end),
510 minimal_drive_time,
511 description);
512}
513
514void Kv1Parser::parseLink() {
515 auto data_owner_code = eatString("LINK.DataOwnerCode", true, 10);
516 auto user_stop_code_begin = eatString("LINK.UserStopCodeBegin", true, 10);
517 auto user_stop_code_end = eatString("LINK.UserStopCodeEnd", true, 10);
518 eatCell("LINK.<deprecated field #1>" );
519 auto distance = eatNumber("LINK.Distance", true, 6);
520 auto description = eatString("LINK.Description", false, 255);
521 auto transport_type = eatString("LINK.TransportType", true, 5);
522 if (!record_errors.empty()) return;
523
524 records.links.emplace_back(
525 Kv1Link::Key(
526 data_owner_code,
527 user_stop_code_begin,
528 user_stop_code_end,
529 transport_type),
530 *distance,
531 description);
532}
533
534void Kv1Parser::parseLine() {
535 auto data_owner_code = eatString ("LINE.DataOwnerCode", true, 10);
536 auto line_planning_number = eatString ("LINE.LinePlanningNumber", true, 10);
537 auto line_public_number = eatString ("LINE.LinePublicNumber", true, 4);
538 auto line_name = eatString ("LINE.LineName", true, 50);
539 auto line_ve_tag_number = eatNumber ("LINE.LineVeTagNumber", true, 3);
540 auto description = eatString ("LINE.Description", false, 255);
541 auto transport_type = eatString ("LINE.TransportType", true, 5);
542 auto line_icon = eatNumber ("LINE.LineIcon", false, 4);
543 auto line_color = eatRgbColor("LINE.LineColor", false );
544 auto line_text_color = eatRgbColor("LINE.LineTextColor", false );
545 if (!record_errors.empty()) return;
546
547 // NOTE: This check, although it should be performed to comply with the
548 // specification, is not actually honored by transit operators (such as
549 // Connexxion) :/ That's enough reason to keep it disabled here for now.
550 // if (*line_ve_tag_number < 0 || *line_ve_tag_number > 399) {
551 // record_errors.push_back(std::format("LINE.LineVeTagNumber is out of range [0-399] with value {}", *line_ve_tag_number));
552 // return;
553 // }
554 if (*line_ve_tag_number != static_cast<short>(*line_ve_tag_number))
555 record_errors.push_back("LINE.LineVeTagNumber should be an integer");
556 if (line_icon && *line_icon != static_cast<short>(*line_icon))
557 record_errors.push_back("LINE.LineIcon should be an integer");
558 if (!record_errors.empty()) return;
559
560 records.lines.emplace_back(
561 Kv1Line::Key(
562 data_owner_code,
563 line_planning_number),
564 line_public_number,
565 line_name,
566 static_cast<short>(*line_ve_tag_number),
567 description,
568 transport_type,
569 static_cast<std::optional<short>>(line_icon),
570 line_color,
571 line_text_color);
572}
573
574void Kv1Parser::parseDestination() {
575 auto data_owner_code = eatString ("DEST.DataOwnerCode", true, 10);
576 auto dest_code = eatString ("DEST.DestCode", true, 10);
577 auto dest_name_full = eatString ("DEST.DestNameFull", true, 50);
578 auto dest_name_main = eatString ("DEST.DestNameMain", true, 24);
579 auto dest_name_detail = eatString ("DEST.DestNameDetail", false, 24);
580 auto relevant_dest_name_detail = eatBoolean ("DEST.RelevantDestNameDetail", true );
581 auto dest_name_main_21 = eatString ("DEST.DestNameMain21", true, 21);
582 auto dest_name_detail_21 = eatString ("DEST.DestNameDetail21", false, 21);
583 auto dest_name_main_19 = eatString ("DEST.DestNameMain19", true, 19);
584 auto dest_name_detail_19 = eatString ("DEST.DestNameDetail19", false, 19);
585 auto dest_name_main_16 = eatString ("DEST.DestNameMain16", true, 16);
586 auto dest_name_detail_16 = eatString ("DEST.DestNameDetail16", false, 16);
587 auto dest_icon = eatNumber ("DEST.DestIcon", false, 4);
588 auto dest_color = eatRgbColor("DEST.DestColor", false );
589 // NOTE: Deviating from the offical KV1 specification here. It specifies that
590 // the maximum length for this field should be 30, but then proceeds to
591 // specify that it should contain a RGB value comprising of three
592 // hexadecimally encoded octets, i.e. six characters. We assume that the
593 // latter is correct and the intended interpretation.
594 auto dest_text_color = eatRgbColor("DEST.DestTextColor", false );
595 if (!record_errors.empty()) return;
596
597 if (dest_icon && *dest_icon != static_cast<short>(*dest_icon)) {
598 record_errors.push_back("DEST.DestIcon should be an integer");
599 return;
600 }
601
602 records.destinations.emplace_back(
603 Kv1Destination::Key(
604 data_owner_code,
605 dest_code),
606 dest_name_full,
607 dest_name_main,
608 dest_name_detail,
609 *relevant_dest_name_detail,
610 dest_name_main_21,
611 dest_name_detail_21,
612 dest_name_main_19,
613 dest_name_detail_19,
614 dest_name_main_16,
615 dest_name_detail_16,
616 dest_icon,
617 dest_color,
618 dest_text_color);
619}
620
621void Kv1Parser::parseJourneyPattern() {
622 auto data_owner_code = eatString("JOPA.DataOwnerCode", true, 10);
623 auto line_planning_number = eatString("JOPA.LinePlanningNumber", true, 10);
624 auto journey_pattern_code = eatString("JOPA.JourneyPatternCode", true, 10);
625 auto journey_pattern_type = eatString("JOPA.JourneyPatternType", true, 10);
626 auto direction = eatString("JOPA.Direction", true, 1);
627 auto description = eatString("JOPA.Description", false, 255);
628 if (!record_errors.empty()) return;
629
630 if (direction != "1" && direction != "2" && direction != "A" && direction != "B") {
631 record_errors.push_back("JOPA.Direction should be in [1, 2, A, B]");
632 return;
633 }
634
635 records.journey_patterns.emplace_back(
636 Kv1JourneyPattern::Key(
637 data_owner_code,
638 line_planning_number,
639 journey_pattern_code),
640 journey_pattern_type,
641 direction[0],
642 description);
643}
644
645void Kv1Parser::parseConcessionFinancerRelation() {
646 auto data_owner_code = eatString("CONFINREL.DataOwnerCode", true, 10);
647 auto con_fin_rel_code = eatString("CONFINREL.ConFinRelCode", true, 10);
648 auto concession_area_code = eatString("CONFINREL.ConcessionAreaCode", true, 10);
649 auto financer_code = eatString("CONFINREL.FinancerCode", false, 10);
650 if (!record_errors.empty()) return;
651
652 records.concession_financer_relations.emplace_back(
653 Kv1ConcessionFinancerRelation::Key(
654 data_owner_code,
655 con_fin_rel_code),
656 concession_area_code,
657 financer_code);
658}
659
660void Kv1Parser::parseConcessionArea() {
661 auto data_owner_code = eatString("CONAREA.DataOwnerCode", true, 10);
662 auto concession_area_code = eatString("CONAREA.ConcessionAreaCode", true, 10);
663 auto description = eatString("CONAREA.Description", true, 255);
664 if (!record_errors.empty()) return;
665
666 records.concession_areas.emplace_back(
667 Kv1ConcessionArea::Key(
668 data_owner_code,
669 concession_area_code),
670 description);
671}
672
673void Kv1Parser::parseFinancer() {
674 auto data_owner_code = eatString("FINANCER.DataOwnerCode", true, 10);
675 auto financer_code = eatString("FINANCER.FinancerCode", true, 10);
676 auto description = eatString("FINANCER.Description", true, 255);
677 if (!record_errors.empty()) return;
678
679 records.financers.emplace_back(
680 Kv1Financer::Key(
681 data_owner_code,
682 financer_code),
683 description);
684}
685
686void Kv1Parser::parseJourneyPatternTimingLink() {
687 auto data_owner_code = eatString ("JOPATILI.DataOwnerCode", true, 10);
688 auto line_planning_number = eatString ("JOPATILI.LinePlanningNumber", true, 10);
689 auto journey_pattern_code = eatString ("JOPATILI.JourneyPatternCode", true, 10);
690 auto timing_link_order = eatNumber ("JOPATILI.TimingLinkOrder", true, 3);
691 auto user_stop_code_begin = eatString ("JOPATILI.UserStopCodeBegin", true, 10);
692 auto user_stop_code_end = eatString ("JOPATILI.UserStopCodeEnd", true, 10);
693 auto con_fin_rel_code = eatString ("JOPATILI.ConFinRelCode", true, 10);
694 auto dest_code = eatString ("JOPATILI.DestCode", true, 10);
695 eatCell ("JOPATILI.<deprecated field #1>" );
696 auto is_timing_stop = eatBoolean ("JOPATILI.IsTimingStop", true );
697 auto display_public_line = eatString ("JOPATILI.DisplayPublicLine", false, 4);
698 auto product_formula_type = eatNumber ("JOPATILI.ProductFormulaType", false, 4);
699 auto get_in = eatBoolean ("JOPATILI.GetIn", true );
700 auto get_out = eatBoolean ("JOPATILI.GetOut", true );
701 auto show_flexible_trip = eatString ("JOPATILI.ShowFlexibleTrip", false, 8);
702 auto line_dest_icon = eatNumber ("JOPATILI.LineDestIcon", false, 4);
703 auto line_dest_color = eatRgbColor("JOPATILI.LineDestColor", false );
704 auto line_dest_text_color = eatRgbColor("JOPATILI.LineDestTextColor", false );
705 if (!record_errors.empty()) return;
706
707 if (line_dest_icon && *line_dest_icon != static_cast<short>(*line_dest_icon))
708 record_errors.push_back("JOPATILI.LineDestIcon should be an integer");
709 if (!show_flexible_trip.empty() && show_flexible_trip != "TRUE" &&
710 show_flexible_trip != "FALSE" && show_flexible_trip != "REALTIME")
711 record_errors.push_back("JOPATILI.ShowFlexibleTrip should be in BISON E21 values [TRUE, FALSE, REALTIME]");
712 if (!record_errors.empty()) return;
713
714 records.journey_pattern_timing_links.emplace_back(
715 Kv1JourneyPatternTimingLink::Key(
716 data_owner_code,
717 line_planning_number,
718 journey_pattern_code,
719 static_cast<short>(*timing_link_order)),
720 user_stop_code_begin,
721 user_stop_code_end,
722 con_fin_rel_code,
723 dest_code,
724 *is_timing_stop,
725 display_public_line,
726 product_formula_type,
727 *get_in,
728 *get_out,
729 show_flexible_trip,
730 line_dest_icon,
731 line_dest_color,
732 line_dest_text_color);
733}
734
735void Kv1Parser::parsePoint() {
736 auto data_owner_code = eatString("POINT.DataOwnerCode", true, 10);
737 auto point_code = eatString("POINT.PointCode", true, 10);
738 eatCell ("POINT.<deprecated field #1>" );
739 auto point_type = eatString("POINT.PointType", true, 10);
740 auto coordinate_system_type = eatString("POINT.CoordinateSystemType", true, 10);
741 // NOTE: We deviate from the specification here once again. The specification
742 // notes that LocationX_EW should contain 'at least 6 positions'. Assuming
743 // that this is referring to the amount of digits, we have to lower this to
744 // 4. Otherwise, some positions in the Netherlands and Belgium are
745 // unrepresentable.
746 auto location_x_ew = eatRdCoord("POINT.LocationX_EW", true, 4);
747 auto location_y_ew = eatRdCoord("POINT.LocationX_EW", true, 6);
748 auto location_z = eatRdCoord("POINT.LocationZ", false, 0);
749 auto description = eatString ("POINT.Description", false, 255);
750 if (!record_errors.empty()) return;
751
752 records.points.emplace_back(
753 Kv1Point::Key(
754 std::move(data_owner_code),
755 std::move(point_code)),
756 std::move(point_type),
757 std::move(coordinate_system_type),
758 *location_x_ew,
759 *location_y_ew,
760 location_z,
761 std::move(description));
762}
763
764void Kv1Parser::parsePointOnLink() {
765 auto data_owner_code = eatString("POOL.DataOwnerCode", true, 10);
766 auto user_stop_code_begin = eatString("POOL.UserStopCodeBegin", true, 10);
767 auto user_stop_code_end = eatString("POOL.UserStopCodeEnd", true, 10);
768 eatCell ("POOL.<deprecated field #1>" );
769 auto point_data_owner_code = eatString("POOL.PointDataOwnerCode", true, 10);
770 auto point_code = eatString("POOL.PointCode", true, 10);
771 auto distance_since_start_of_link = eatNumber("POOL.DistanceSinceStartOfLink", true, 5);
772 auto segment_speed = eatNumber("POOL.SegmentSpeed", false, 4);
773 auto local_point_speed = eatNumber("POOL.LocalPointSpeed", false, 4);
774 auto description = eatString("POOL.Description", false, 255);
775 auto transport_type = eatString("POOL.TransportType", true, 5);
776 if (!record_errors.empty()) return;
777
778 records.point_on_links.emplace_back(
779 Kv1PointOnLink::Key(
780 data_owner_code,
781 user_stop_code_begin,
782 user_stop_code_end,
783 point_data_owner_code,
784 point_code,
785 transport_type),
786 *distance_since_start_of_link,
787 segment_speed,
788 local_point_speed,
789 std::move(description));
790}
791
792void Kv1Parser::parseIcon() {
793 auto data_owner_code = eatString("ICON.DataOwnerCode", true, 10);
794 auto icon_number = eatNumber("ICON.IconNumber", true, 4);
795 auto icon_uri = eatString("ICON.IconURI", true, 1024);
796 if (!record_errors.empty()) return;
797
798 if (*icon_number != static_cast<short>(*icon_number)) {
799 record_errors.push_back("ICON.IconNumber should be an integer");
800 return;
801 }
802
803 records.icons.emplace_back(
804 Kv1Icon::Key(
805 data_owner_code,
806 static_cast<short>(*icon_number)),
807 icon_uri);
808}
809
810void Kv1Parser::parseNotice() {
811 auto data_owner_code = eatString("NOTICE.DataOwnerCode", true, 10);
812 auto notice_code = eatString("NOTICE.NoticeCode", true, 20);
813 auto notice_content = eatString("NOTICE.NoticeContent", true, 1024);
814 if (!record_errors.empty()) return;
815
816 records.notices.emplace_back(
817 Kv1Notice::Key(
818 data_owner_code,
819 notice_code),
820 notice_content);
821}
822
823void Kv1Parser::parseNoticeAssignment() {
824 auto data_owner_code = eatString("NTCASSGNM.DataOwnerCode", true, 10);
825 auto notice_code = eatString("NTCASSGNM.NoticeCode", true, 20);
826 auto assigned_object = eatString("NTCASSGNM.AssignedObject", true, 8);
827 auto timetable_version_code = eatString("NTCASSGNM.TimetableVersionCode", false, 10);
828 auto organizational_unit_code = eatString("NTCASSGNM.OrganizationalUnitCode", false, 10);
829 auto schedule_code = eatString("NTCASSGNM.ScheduleCode", false, 10);
830 auto schedule_type_code = eatString("NTCASSGNM.ScheduleTypeCode", false, 10);
831 auto period_group_code = eatString("NTCASSGNM.PeriodGroupCode", false, 10);
832 auto specific_day_code = eatString("NTCASSGNM.SpecificDayCode", false, 10);
833 auto day_type = eatString("NTCASSGNM.DayType", false, 7);
834 auto line_planning_number = eatString("NTCASSGNM.LinePlanningNumber", true, 10);
835 auto journey_number = eatNumber("NTCASSGNM.JourneyNumber", false, 6);
836 auto stop_order = eatNumber("NTCASSGNM.StopOrder", false, 4);
837 auto journey_pattern_code = eatString("NTCASSGNM.JourneyPatternCode", false, 10);
838 auto timing_link_order = eatNumber("NTCASSGNM.TimingLinkOrder", false, 3);
839 auto user_stop_code = eatString("NTCASSGNM.UserStopCode", false, 10);
840 if (!record_errors.empty()) return;
841
842 if (journey_number && *journey_number != static_cast<short>(*journey_number))
843 record_errors.push_back("NTCASSGNM.JourneyNumber should be an integer");
844 if (journey_number && (*journey_number < 0 || *journey_number > 999'999))
845 record_errors.push_back("NTCASSGNM.JourneyNumber should be within the range [0-999999]");
846 if (stop_order && *stop_order != static_cast<short>(*stop_order))
847 record_errors.push_back("NTCASSGNM.StopOrder should be an integer");
848 if (!journey_number && (assigned_object == "PUJO" || assigned_object == "PUJOPASS"))
849 record_errors.push_back("NTCASSGNM.JourneyNumber is required for AssignedObject PUJO/PUJOPASS");
850 if (journey_pattern_code.empty() && assigned_object == "JOPATILI")
851 record_errors.push_back("NTCASSGNM.JourneyPatternCode is required for AssignedObject JOPATILI");
852 if (!record_errors.empty()) return;
853
854 records.notice_assignments.emplace_back(
855 data_owner_code,
856 notice_code,
857 assigned_object,
858 timetable_version_code,
859 organizational_unit_code,
860 schedule_code,
861 schedule_type_code,
862 period_group_code,
863 specific_day_code,
864 day_type,
865 line_planning_number,
866 static_cast<std::optional<int>>(journey_number),
867 static_cast<std::optional<short>>(stop_order),
868 journey_pattern_code,
869 timing_link_order,
870 user_stop_code);
871}
872
873void Kv1Parser::parseTimeDemandGroup() {
874 auto data_owner_code = eatString("TIMDEMGRP.DataOwnerCode", true, 10);
875 auto line_planning_number = eatString("TIMDEMGRP.LinePlanningNumber", true, 10);
876 auto journey_pattern_code = eatString("TIMDEMGRP.JourneyPatternCode", true, 10);
877 auto time_demand_group_code = eatString("TIMDEMGRP.TimeDemandGroupCode", true, 10);
878 if (!record_errors.empty()) return;
879
880 records.time_demand_groups.emplace_back(
881 Kv1TimeDemandGroup::Key(
882 data_owner_code,
883 line_planning_number,
884 journey_pattern_code,
885 time_demand_group_code));
886}
887
888void Kv1Parser::parseTimeDemandGroupRunTime() {
889 auto data_owner_code = eatString("TIMDEMRNT.DataOwnerCode", true, 10);
890 auto line_planning_number = eatString("TIMDEMRNT.LinePlanningNumber", true, 10);
891 auto journey_pattern_code = eatString("TIMDEMRNT.JourneyPatternCode", true, 10);
892 auto time_demand_group_code = eatString("TIMDEMRNT.TimeDemandGroupCode", true, 10);
893 auto timing_link_order = eatNumber("TIMDEMRNT.TimingLinkOrder", true, 3);
894 auto user_stop_code_begin = eatString("TIMDEMRNT.UserStopCodeBegin", true, 10);
895 auto user_stop_code_end = eatString("TIMDEMRNT.UserStopCodeEnd", true, 10);
896 auto total_drive_time = eatNumber("TIMDEMRNT.TotalDriveTime", true, 5);
897 auto drive_time = eatNumber("TIMDEMRNT.DriveTime", true, 5);
898 auto expected_delay = eatNumber("TIMDEMRNT.ExpectedDelay", false, 5);
899 auto layover_time = eatNumber("TIMDEMRNT.LayOverTime", false, 5);
900 auto stop_wait_time = eatNumber("TIMDEMRNT.StopWaitTime", true, 5);
901 auto minimum_stop_time = eatNumber("TIMDEMRNT.MinimumStopTime", false, 5);
902 if (!record_errors.empty()) return;
903
904 if (timing_link_order && *timing_link_order != static_cast<short>(*timing_link_order)) {
905 record_errors.push_back("TIMDEMRNT.TimingLinkOrder should be an integer");
906 return;
907 }
908
909 records.time_demand_group_run_times.emplace_back(
910 Kv1TimeDemandGroupRunTime::Key(
911 data_owner_code,
912 line_planning_number,
913 journey_pattern_code,
914 time_demand_group_code,
915 static_cast<short>(*timing_link_order)),
916 user_stop_code_begin,
917 user_stop_code_end,
918 *total_drive_time,
919 *drive_time,
920 expected_delay,
921 layover_time,
922 *stop_wait_time,
923 minimum_stop_time);
924}
925
926void Kv1Parser::parsePeriodGroup() {
927 auto data_owner_code = eatString("PEGR.DataOwnerCode", true, 10);
928 auto period_group_code = eatString("PEGR.PeriodGroupCode", true, 10);
929 auto description = eatString("PEGR.Description", false, 255);
930 if (!record_errors.empty()) return;
931
932 records.period_groups.emplace_back(
933 Kv1PeriodGroup::Key(
934 data_owner_code,
935 period_group_code),
936 description);
937}
938
939void Kv1Parser::parseSpecificDay() {
940 auto data_owner_code = eatString("SPECDAY.DataOwnerCode", true, 10);
941 auto specific_day_code = eatString("SPECDAY.SpecificDayCode", true, 10);
942 auto name = eatString("SPECDAY.Name", true, 50);
943 auto description = eatString("SPECDAY.Description", false, 255);
944 if (!record_errors.empty()) return;
945
946 records.specific_days.emplace_back(
947 Kv1SpecificDay::Key(
948 data_owner_code,
949 specific_day_code),
950 name,
951 description);
952}
953
954void Kv1Parser::parseTimetableVersion() {
955 auto data_owner_code = eatString("TIVE.DataOwnerCode", true, 10);
956 auto organizational_unit_code = eatString("TIVE.OrganizationalUnitCode", true, 10);
957 auto timetable_version_code = eatString("TIVE.TimetableVersionCode", true, 10);
958 auto period_group_code = eatString("TIVE.PeriodGroupCode", true, 10);
959 auto specific_day_code = eatString("TIVE.SpecificDayCode", true, 10);
960 auto valid_from_raw = eatString("TIVE.ValidFrom", true, 10);
961 auto timetable_version_type = eatString("TIVE.TimetableVersionType", true, 10);
962 auto valid_thru_raw = eatString("TIVE.ValidThru", false, 10);
963 auto description = eatString("TIVE.Description", false, 255);
964 if (!record_errors.empty()) return;
965
966 auto valid_from = parseYyyymmdd(valid_from_raw);
967 if (!valid_from)
968 record_errors.push_back("TIVE.ValidFrom has invalid format, should be YYYY-MM-DD");
969 std::optional<std::chrono::year_month_day> valid_thru;
970 if (!valid_thru_raw.empty()) {
971 valid_thru = parseYyyymmdd(valid_thru_raw);
972 if (!valid_thru) {
973 record_errors.push_back("TIVE.ValidFrom has invalid format, should be YYYY-MM-DD");
974 }
975 }
976 if (!description.empty())
977 record_errors.push_back("TIVE.Description should be empty");
978 if (!record_errors.empty()) return;
979
980 records.timetable_versions.emplace_back(
981 Kv1TimetableVersion::Key(
982 data_owner_code,
983 organizational_unit_code,
984 timetable_version_code,
985 period_group_code,
986 specific_day_code),
987 *valid_from,
988 timetable_version_type,
989 valid_thru,
990 description);
991}
992
993void Kv1Parser::parsePublicJourney() {
994 auto data_owner_code = eatString ("PUJO.DataOwnerCode", true, 10);
995 auto timetable_version_code = eatString ("PUJO.TimetableVersionCode", true, 10);
996 auto organizational_unit_code = eatString ("PUJO.OrganizationalUnitCode", true, 10);
997 auto period_group_code = eatString ("PUJO.PeriodGroupCode", true, 10);
998 auto specific_day_code = eatString ("PUJO.SpecificDayCode", true, 10);
999 auto day_type = eatString ("PUJO.DayType", true, 7);
1000 auto line_planning_number = eatString ("PUJO.LinePlanningNumber", true, 10);
1001 auto journey_number = eatNumber ("PUJO.JourneyNumber", true, 6);
1002 auto time_demand_group_code = eatString ("PUJO.TimeDemandGroupCode", true, 10);
1003 auto journey_pattern_code = eatString ("PUJO.JourneyPatternCode", true, 10);
1004 auto departure_time_raw = eatString ("PUJO.DepartureTime", true, 8);
1005 auto wheelchair_accessible = eatString ("PUJO.WheelChairAccessible", true, 13);
1006 auto data_owner_is_operator = eatBoolean("PUJO.DataOwnerIsOperator", true );
1007 auto planned_monitored = eatBoolean("PUJO.PlannedMonitored", true );
1008 auto product_formula_type = eatNumber ("PUJO.ProductFormulaType", false, 4);
1009 auto show_flexible_trip = eatString ("PUJO.ShowFlexibleTrip", false, 8);
1010 if (!record_errors.empty()) return;
1011
1012 auto departure_time = parseHhmmss(departure_time_raw);
1013 if (!departure_time)
1014 record_errors.push_back("PUJO.DepartureTime has a bad format");
1015 if (*journey_number < 0 || *journey_number > 999'999)
1016 record_errors.push_back("PUJO.JourneyNumber should be within the range [0-999999]");
1017 if (*journey_number != static_cast<int>(*journey_number))
1018 record_errors.push_back("PUJO.JourneyNumber should be an integer");
1019 if (product_formula_type && *product_formula_type != static_cast<short>(*product_formula_type))
1020 record_errors.push_back("PUJO.ProductFormulaType should be an integer");
1021 if (wheelchair_accessible != "ACCESSIBLE" && wheelchair_accessible != "NOTACCESSIBLE" && wheelchair_accessible != "UNKNOWN")
1022 record_errors.push_back("PUJO.WheelChairAccessible should be in BISON E3 values [ACCESSIBLE, NOTACCESSIBLE, UNKNOWN]");
1023 if (!show_flexible_trip.empty() && show_flexible_trip != "TRUE" &&
1024 show_flexible_trip != "FALSE" && show_flexible_trip != "REALTIME")
1025 record_errors.push_back("PUJO.ShowFlexibleTrip should be in BISON E21 values [TRUE, FALSE, REALTIME]");
1026 if (!record_errors.empty()) return;
1027
1028 records.public_journeys.emplace_back(
1029 Kv1PublicJourney::Key(
1030 data_owner_code,
1031 timetable_version_code,
1032 organizational_unit_code,
1033 period_group_code,
1034 specific_day_code,
1035 day_type,
1036 line_planning_number,
1037 static_cast<int>(*journey_number)),
1038 time_demand_group_code,
1039 journey_pattern_code,
1040 *departure_time,
1041 wheelchair_accessible,
1042 *data_owner_is_operator,
1043 *planned_monitored,
1044 product_formula_type,
1045 show_flexible_trip);
1046}
1047
1048void Kv1Parser::parsePeriodGroupValidity() {
1049 auto data_owner_code = eatString("PEGRVAL.DataOwnerCode", true, 10);
1050 auto organizational_unit_code = eatString("PEGRVAL.OrganizationalUnitCode", true, 10);
1051 auto period_group_code = eatString("PEGRVAL.PeriodGroupCode", true, 10);
1052 auto valid_from_raw = eatString("PEGRVAL.ValidFrom", true, 10);
1053 auto valid_thru_raw = eatString("PEGRVAL.ValidThru", true, 10);
1054 if (!record_errors.empty()) return;
1055
1056 auto valid_from = parseYyyymmdd(valid_from_raw);
1057 auto valid_thru = parseYyyymmdd(valid_thru_raw);
1058 if (!valid_from)
1059 record_errors.push_back("PEGRVAL.ValidFrom has invalid format, should be YYYY-MM-DD");
1060 if (!valid_thru)
1061 record_errors.push_back("PEGRVAL.ValidThru has invalid format, should be YYYY-MM-DD");
1062 if (!record_errors.empty()) return;
1063
1064 records.period_group_validities.emplace_back(
1065 Kv1PeriodGroupValidity::Key(
1066 data_owner_code,
1067 organizational_unit_code,
1068 period_group_code,
1069 *valid_from),
1070 *valid_thru);
1071}
1072
1073void Kv1Parser::parseExceptionalOperatingDay() {
1074 auto data_owner_code = eatString("EXCOPDAY.DataOwnerCode", true, 10);
1075 auto organizational_unit_code = eatString("EXCOPDAY.OrganizationalUnitCode", true, 10);
1076 auto valid_date_raw = eatString("EXCOPDAY.ValidDate", true, 23);
1077 auto day_type_as_on = eatString("EXCOPDAY.DayTypeAsOn", true, 7);
1078 auto specific_day_code = eatString("EXCOPDAY.SpecificDayCode", true, 10);
1079 auto period_group_code = eatString("EXCOPDAY.PeriodGroupCode", false, 10);
1080 auto description = eatString("EXCOPDAY.Description", false, 255);
1081 if (!record_errors.empty()) return;
1082
1083 std::string_view error;
1084 auto valid_date = parseDateTime(valid_date_raw, amsterdam, &error);
1085 if (!valid_date) {
1086 record_errors.push_back(std::format("EXCOPDAY.ValidDate has an bad format (value: {}): {}", valid_date_raw, error));
1087 return;
1088 }
1089
1090 records.exceptional_operating_days.emplace_back(
1091 Kv1ExceptionalOperatingDay::Key(
1092 data_owner_code,
1093 organizational_unit_code,
1094 *valid_date),
1095 day_type_as_on,
1096 specific_day_code,
1097 period_group_code,
1098 description);
1099}
1100
1101void Kv1Parser::parseScheduleVersion() {
1102 auto data_owner_code = eatString("SCHEDVERS.DataOwnerCode", true, 10);
1103 auto organizational_unit_code = eatString("SCHEDVERS.OrganizationalUnitCode", true, 10);
1104 auto schedule_code = eatString("SCHEDVERS.ScheduleCode", true, 10);
1105 auto schedule_type_code = eatString("SCHEDVERS.ScheduleTypeCode", true, 10);
1106 auto valid_from_raw = eatString("SCHEDVERS.ValidFrom", true, 10);
1107 auto valid_thru_raw = eatString("SCHEDVERS.ValidThru", false, 10);
1108 auto description = eatString("SCHEDVERS.Description", false, 255);
1109 if (!record_errors.empty()) return;
1110
1111 auto valid_from = parseYyyymmdd(valid_from_raw);
1112 if (!valid_from)
1113 record_errors.push_back("SCHEDVERS.ValidFrom has invalid format, should be YYYY-MM-DD");
1114 std::optional<std::chrono::year_month_day> valid_thru;
1115 if (!valid_thru_raw.empty()) {
1116 valid_thru = parseYyyymmdd(valid_thru_raw);
1117 if (!valid_thru) {
1118 record_errors.push_back("SCHEDVERS.ValidFrom has invalid format, should be YYYY-MM-DD");
1119 }
1120 }
1121 if (!description.empty())
1122 record_errors.push_back("SCHEDVERS.Description should be empty");
1123 if (!record_errors.empty()) return;
1124
1125 records.schedule_versions.emplace_back(
1126 Kv1ScheduleVersion::Key(
1127 data_owner_code,
1128 organizational_unit_code,
1129 schedule_code,
1130 schedule_type_code),
1131 *valid_from,
1132 valid_thru,
1133 description);
1134}
1135
1136void Kv1Parser::parsePublicJourneyPassingTimes() {
1137 auto data_owner_code = eatString ("PUJOPASS.DataOwnerCode", true, 10);
1138 auto organizational_unit_code = eatString ("PUJOPASS.OrganizationalUnitCode", true, 10);
1139 auto schedule_code = eatString ("PUJOPASS.ScheduleCode", true, 10);
1140 auto schedule_type_code = eatString ("PUJOPASS.ScheduleTypeCode", true, 10);
1141 auto line_planning_number = eatString ("PUJOPASS.LinePlanningNumber", true, 10);
1142 auto journey_number = eatNumber ("PUJOPASS.JourneyNumber", true, 6);
1143 auto stop_order = eatNumber ("PUJOPASS.StopOrder", true, 4);
1144 auto journey_pattern_code = eatString ("PUJOPASS.JourneyPatternCode", true, 10);
1145 auto user_stop_code = eatString ("PUJOPASS.UserStopCode", true, 10);
1146 auto target_arrival_time_raw = eatString ("PUJOPASS.TargetArrivalTime", false, 8);
1147 auto target_departure_time_raw = eatString ("PUJOPASS.TargetDepartureTime", false, 8);
1148 auto wheelchair_accessible = eatString ("PUJOPASS.WheelChairAccessible", true, 13);
1149 auto data_owner_is_operator = eatBoolean("PUJOPASS.DataOwnerIsOperator", true );
1150 auto planned_monitored = eatBoolean("PUJOPASS.PlannedMonitored", true );
1151 auto product_formula_type = eatNumber ("PUJOPASS.ProductFormulaType", false, 4);
1152 auto show_flexible_trip = eatString ("PUJOPASS.ShowFlexibleTrip", false, 8);
1153 if (!record_errors.empty()) return;
1154
1155 if (*journey_number < 0 || *journey_number > 999'999)
1156 record_errors.push_back("PUJOPASS.JourneyNumber should be within the range [0-999999]");
1157 if (*journey_number != static_cast<int>(*journey_number))
1158 record_errors.push_back("PUJOPASS.JourneyNumber should be an integer");
1159 if (*stop_order != static_cast<short>(*stop_order))
1160 record_errors.push_back("PUJOPASS.StopOrder should be an integer");
1161 if (product_formula_type && *product_formula_type != static_cast<short>(*product_formula_type))
1162 record_errors.push_back("PUJOPASS.ProductFormulaType should be an integer");
1163 if (wheelchair_accessible != "ACCESSIBLE" && wheelchair_accessible != "NOTACCESSIBLE" && wheelchair_accessible != "UNKNOWN")
1164 record_errors.push_back("PUJOPASS.WheelChairAccessible should be in BISON E3 values [ACCESSIBLE, NOTACCESSIBLE, UNKNOWN]");
1165 if (!show_flexible_trip.empty() && show_flexible_trip != "TRUE" &&
1166 show_flexible_trip != "FALSE" && show_flexible_trip != "REALTIME")
1167 record_errors.push_back("PUJOPASS.ShowFlexibleTrip should be in BISON E21 values [TRUE, FALSE, REALTIME]");
1168 std::optional<std::chrono::hh_mm_ss<std::chrono::seconds>> target_arrival_time;
1169 if (!target_arrival_time_raw.empty()) {
1170 target_arrival_time = parseHhmmss(target_arrival_time_raw);
1171 if (!target_arrival_time) {
1172 record_errors.push_back("PUJOPASS.TargetArrivalTime has invalid format, should be HH:MM:SS");
1173 }
1174 }
1175 std::optional<std::chrono::hh_mm_ss<std::chrono::seconds>> target_departure_time;
1176 if (!target_departure_time_raw.empty()) {
1177 target_departure_time = parseHhmmss(target_departure_time_raw);
1178 if (!target_departure_time) {
1179 record_errors.push_back("PUJOPASS.TargetDepartureTime has invalid format, should be HH:MM:SS");
1180 }
1181 }
1182 if (!record_errors.empty()) return;
1183
1184 records.public_journey_passing_times.emplace_back(
1185 Kv1PublicJourneyPassingTimes::Key(
1186 data_owner_code,
1187 organizational_unit_code,
1188 schedule_code,
1189 schedule_type_code,
1190 line_planning_number,
1191 static_cast<int>(*journey_number),
1192 static_cast<short>(*stop_order)),
1193 journey_pattern_code,
1194 user_stop_code,
1195 target_arrival_time,
1196 target_departure_time,
1197 wheelchair_accessible,
1198 *data_owner_is_operator,
1199 *planned_monitored,
1200 product_formula_type,
1201 show_flexible_trip);
1202}
1203
1204void Kv1Parser::parseOperatingDay() {
1205 auto data_owner_code = eatString("OPERDAY.DataOwnerCode", true, 10);
1206 auto organizational_unit_code = eatString("OPERDAY.OrganizationalUnitCode", true, 10);
1207 auto schedule_code = eatString("OPERDAY.ScheduleCode", true, 10);
1208 auto schedule_type_code = eatString("OPERDAY.ScheduleTypeCode", true, 10);
1209 auto valid_date_raw = eatString("OPERDAY.ValidDate", true, 10);
1210 auto description = eatString("OPERDAY.Description", false, 255);
1211 if (!record_errors.empty()) return;
1212
1213 auto valid_date = parseYyyymmdd(valid_date_raw);
1214 if (!valid_date)
1215 record_errors.push_back("OPERDAY.ValidDate has invalid format, should be YYYY-MM-DD");
1216 if (!record_errors.empty()) return;
1217
1218 records.operating_days.emplace_back(
1219 Kv1OperatingDay::Key(
1220 data_owner_code,
1221 organizational_unit_code,
1222 schedule_code,
1223 schedule_type_code,
1224 *valid_date),
1225 description);
1226}
1227
1228const std::unordered_map<std::string_view, Kv1Parser::ParseFunc> Kv1Parser::type_parsers{
1229 { "ORUN", &Kv1Parser::parseOrganizationalUnit },
1230 { "ORUNORUN", &Kv1Parser::parseHigherOrganizationalUnit },
1231 { "USRSTOP", &Kv1Parser::parseUserStopPoint },
1232 { "USRSTAR", &Kv1Parser::parseUserStopArea },
1233 { "TILI", &Kv1Parser::parseTimingLink },
1234 { "LINK", &Kv1Parser::parseLink },
1235 { "LINE", &Kv1Parser::parseLine },
1236 { "DEST", &Kv1Parser::parseDestination },
1237 { "JOPA", &Kv1Parser::parseJourneyPattern },
1238 { "CONFINREL", &Kv1Parser::parseConcessionFinancerRelation },
1239 { "CONAREA", &Kv1Parser::parseConcessionArea },
1240 { "FINANCER", &Kv1Parser::parseFinancer },
1241 { "JOPATILI", &Kv1Parser::parseJourneyPatternTimingLink },
1242 { "POINT", &Kv1Parser::parsePoint },
1243 { "POOL", &Kv1Parser::parsePointOnLink },
1244 { "ICON", &Kv1Parser::parseIcon },
1245 { "NOTICE", &Kv1Parser::parseNotice },
1246 { "NTCASSGNM", &Kv1Parser::parseNoticeAssignment },
1247 { "TIMDEMGRP", &Kv1Parser::parseTimeDemandGroup },
1248 { "TIMDEMRNT", &Kv1Parser::parseTimeDemandGroupRunTime },
1249 { "PEGR", &Kv1Parser::parsePeriodGroup },
1250 { "SPECDAY", &Kv1Parser::parseSpecificDay },
1251 { "TIVE", &Kv1Parser::parseTimetableVersion },
1252 { "PUJO", &Kv1Parser::parsePublicJourney },
1253 { "PEGRVAL", &Kv1Parser::parsePeriodGroupValidity },
1254 { "EXCOPDAY", &Kv1Parser::parseExceptionalOperatingDay },
1255 { "SCHEDVERS", &Kv1Parser::parseScheduleVersion },
1256 { "PUJOPASS", &Kv1Parser::parsePublicJourneyPassingTimes },
1257 { "OPERDAY", &Kv1Parser::parseOperatingDay },
1258};
diff --git a/lib/libtmi8/src/kv1_types.cpp b/lib/libtmi8/src/kv1_types.cpp
new file mode 100644
index 0000000..49e306e
--- /dev/null
+++ b/lib/libtmi8/src/kv1_types.cpp
@@ -0,0 +1,773 @@
1// vim:set sw=2 ts=2 sts et:
2
3#include <boost/container_hash/hash.hpp>
4
5#include <tmi8/kv1_types.hpp>
6
7size_t Kv1Records::size() const {
8 return organizational_units.size()
9 + higher_organizational_units.size()
10 + user_stop_points.size()
11 + user_stop_areas.size()
12 + timing_links.size()
13 + links.size()
14 + lines.size()
15 + destinations.size()
16 + journey_patterns.size()
17 + concession_financer_relations.size()
18 + concession_areas.size()
19 + financers.size()
20 + journey_pattern_timing_links.size()
21 + points.size()
22 + point_on_links.size()
23 + icons.size()
24 + notices.size()
25 + notice_assignments.size()
26 + time_demand_groups.size()
27 + time_demand_group_run_times.size()
28 + period_groups.size()
29 + specific_days.size()
30 + timetable_versions.size()
31 + public_journeys.size()
32 + period_group_validities.size()
33 + exceptional_operating_days.size()
34 + schedule_versions.size()
35 + public_journey_passing_times.size()
36 + operating_days.size();
37}
38
39Kv1OrganizationalUnit::Key::Key(
40 std::string data_owner_code,
41 std::string organizational_unit_code)
42 : data_owner_code(std::move(data_owner_code)),
43 organizational_unit_code(std::move(organizational_unit_code))
44{}
45
46Kv1HigherOrganizationalUnit::Key::Key(
47 std::string data_owner_code,
48 std::string organizational_unit_code_parent,
49 std::string organizational_unit_code_child,
50 std::chrono::year_month_day valid_from)
51 : data_owner_code(std::move(data_owner_code)),
52 organizational_unit_code_parent(std::move(organizational_unit_code_parent)),
53 organizational_unit_code_child(std::move(organizational_unit_code_child)),
54 valid_from(valid_from)
55{}
56
57Kv1UserStopPoint::Key::Key(
58 std::string data_owner_code,
59 std::string user_stop_code)
60 : data_owner_code(std::move(data_owner_code)),
61 user_stop_code(std::move(user_stop_code))
62{}
63
64Kv1UserStopArea::Key::Key(
65 std::string data_owner_code,
66 std::string user_stop_area_code)
67 : data_owner_code(std::move(data_owner_code)),
68 user_stop_area_code(std::move(user_stop_area_code))
69{}
70
71Kv1TimingLink::Key::Key(
72 std::string data_owner_code,
73 std::string user_stop_code_begin,
74 std::string user_stop_code_end)
75 : data_owner_code(std::move(data_owner_code)),
76 user_stop_code_begin(std::move(user_stop_code_begin)),
77 user_stop_code_end(std::move(user_stop_code_end))
78{}
79
80Kv1Link::Key::Key(std::string data_owner_code,
81 std::string user_stop_code_begin,
82 std::string user_stop_code_end,
83 std::string transport_type)
84 : data_owner_code(std::move(data_owner_code)),
85 user_stop_code_begin(std::move(user_stop_code_begin)),
86 user_stop_code_end(std::move(user_stop_code_end)),
87 transport_type(std::move(transport_type))
88{}
89
90Kv1Line::Key::Key(std::string data_owner_code,
91 std::string line_planning_number)
92 : data_owner_code(std::move(data_owner_code)),
93 line_planning_number(std::move(line_planning_number))
94{}
95
96Kv1Destination::Key::Key(std::string data_owner_code,
97 std::string dest_code)
98 : data_owner_code(std::move(data_owner_code)),
99 dest_code(std::move(dest_code))
100{}
101
102Kv1JourneyPattern::Key::Key(std::string data_owner_code,
103 std::string line_planning_number,
104 std::string journey_pattern_code)
105 : data_owner_code(std::move(data_owner_code)),
106 line_planning_number(std::move(line_planning_number)),
107 journey_pattern_code(std::move(journey_pattern_code))
108{}
109
110Kv1ConcessionFinancerRelation::Key::Key(std::string data_owner_code,
111 std::string con_fin_rel_code)
112 : data_owner_code(std::move(data_owner_code)),
113 con_fin_rel_code(std::move(con_fin_rel_code))
114{}
115
116Kv1ConcessionArea::Key::Key(std::string data_owner_code,
117 std::string concession_area_code)
118 : data_owner_code(std::move(data_owner_code)),
119 concession_area_code(std::move(concession_area_code))
120{}
121
122Kv1Financer::Key::Key(std::string data_owner_code,
123 std::string financer_code)
124 : data_owner_code(std::move(data_owner_code)),
125 financer_code(std::move(financer_code))
126{}
127
128Kv1JourneyPatternTimingLink::Key::Key(std::string data_owner_code,
129 std::string line_planning_number,
130 std::string journey_pattern_code,
131 short timing_link_order)
132 : data_owner_code(std::move(data_owner_code)),
133 line_planning_number(std::move(line_planning_number)),
134 journey_pattern_code(journey_pattern_code),
135 timing_link_order(timing_link_order)
136{}
137
138Kv1Point::Key::Key(std::string data_owner_code,
139 std::string point_code)
140 : data_owner_code(std::move(data_owner_code)),
141 point_code(std::move(point_code))
142{}
143
144Kv1PointOnLink::Key::Key(std::string data_owner_code,
145 std::string user_stop_code_begin,
146 std::string user_stop_code_end,
147 std::string point_data_owner_code,
148 std::string point_code,
149 std::string transport_type)
150 : data_owner_code(std::move(data_owner_code)),
151 user_stop_code_begin(std::move(user_stop_code_begin)),
152 user_stop_code_end(std::move(user_stop_code_end)),
153 point_data_owner_code(std::move(point_data_owner_code)),
154 point_code(std::move(point_code)),
155 transport_type(std::move(transport_type))
156{}
157
158Kv1Icon::Key::Key(std::string data_owner_code,
159 short icon_number)
160 : data_owner_code(std::move(data_owner_code)),
161 icon_number(icon_number)
162{}
163
164Kv1Notice::Key::Key(std::string data_owner_code,
165 std::string notice_code)
166 : data_owner_code(std::move(data_owner_code)),
167 notice_code(std::move(notice_code))
168{}
169
170Kv1TimeDemandGroup::Key::Key(std::string data_owner_code,
171 std::string line_planning_number,
172 std::string journey_pattern_code,
173 std::string time_demand_group_code)
174 : data_owner_code(std::move(data_owner_code)),
175 line_planning_number(std::move(line_planning_number)),
176 journey_pattern_code(std::move(journey_pattern_code)),
177 time_demand_group_code(std::move(time_demand_group_code))
178{}
179
180Kv1TimeDemandGroupRunTime::Key::Key(std::string data_owner_code,
181 std::string line_planning_number,
182 std::string journey_pattern_code,
183 std::string time_demand_group_code,
184 short timing_link_order)
185 : data_owner_code(std::move(data_owner_code)),
186 line_planning_number(std::move(line_planning_number)),
187 journey_pattern_code(std::move(journey_pattern_code)),
188 time_demand_group_code(std::move(time_demand_group_code)),
189 timing_link_order(std::move(timing_link_order))
190{}
191
192Kv1PeriodGroup::Key::Key(std::string data_owner_code,
193 std::string period_group_code)
194 : data_owner_code(std::move(data_owner_code)),
195 period_group_code(std::move(period_group_code))
196{}
197
198Kv1SpecificDay::Key::Key(std::string data_owner_code,
199 std::string specific_day_code)
200 : data_owner_code(std::move(data_owner_code)),
201 specific_day_code(std::move(specific_day_code))
202{}
203
204Kv1TimetableVersion::Key::Key(std::string data_owner_code,
205 std::string organizational_unit_code,
206 std::string timetable_version_code,
207 std::string period_group_code,
208 std::string specific_day_code)
209 : data_owner_code(std::move(data_owner_code)),
210 organizational_unit_code(std::move(organizational_unit_code)),
211 timetable_version_code(std::move(timetable_version_code)),
212 period_group_code(std::move(period_group_code)),
213 specific_day_code(std::move(specific_day_code))
214{}
215
216Kv1PublicJourney::Key::Key(std::string data_owner_code,
217 std::string timetable_version_code,
218 std::string organizational_unit_code,
219 std::string period_group_code,
220 std::string specific_day_code,
221 std::string day_type,
222 std::string line_planning_number,
223 int journey_number)
224 : data_owner_code(std::move(data_owner_code)),
225 timetable_version_code(std::move(timetable_version_code)),
226 organizational_unit_code(std::move(organizational_unit_code)),
227 period_group_code(std::move(period_group_code)),
228 specific_day_code(std::move(specific_day_code)),
229 day_type(std::move(day_type)),
230 line_planning_number(std::move(line_planning_number)),
231 journey_number(journey_number)
232{}
233
234Kv1PeriodGroupValidity::Key::Key(std::string data_owner_code,
235 std::string organizational_unit_code,
236 std::string period_group_code,
237 std::chrono::year_month_day valid_from)
238 : data_owner_code(std::move(data_owner_code)),
239 organizational_unit_code(std::move(organizational_unit_code)),
240 period_group_code(std::move(period_group_code)),
241 valid_from(valid_from)
242{}
243
244Kv1ExceptionalOperatingDay::Key::Key(std::string data_owner_code,
245 std::string organizational_unit_code,
246 std::chrono::sys_seconds valid_date)
247 : data_owner_code(std::move(data_owner_code)),
248 organizational_unit_code(std::move(organizational_unit_code)),
249 valid_date(valid_date)
250{}
251
252Kv1ScheduleVersion::Key::Key(std::string data_owner_code,
253 std::string organizational_unit_code,
254 std::string schedule_code,
255 std::string schedule_type_code)
256 : data_owner_code(std::move(data_owner_code)),
257 organizational_unit_code(std::move(organizational_unit_code)),
258 schedule_code(std::move(schedule_code)),
259 schedule_type_code(std::move(schedule_type_code))
260{}
261
262Kv1PublicJourneyPassingTimes::Key::Key(std::string data_owner_code,
263 std::string organizational_unit_code,
264 std::string schedule_code,
265 std::string schedule_type_code,
266 std::string line_planning_number,
267 int journey_number,
268 short stop_order)
269 : data_owner_code(std::move(data_owner_code)),
270 organizational_unit_code(std::move(organizational_unit_code)),
271 schedule_code(std::move(schedule_code)),
272 schedule_type_code(std::move(schedule_type_code)),
273 line_planning_number(std::move(line_planning_number)),
274 journey_number(journey_number),
275 stop_order(stop_order)
276{}
277
278Kv1OperatingDay::Key::Key(std::string data_owner_code,
279 std::string organizational_unit_code,
280 std::string schedule_code,
281 std::string schedule_type_code,
282 std::chrono::year_month_day valid_date)
283 : data_owner_code(std::move(data_owner_code)),
284 organizational_unit_code(std::move(organizational_unit_code)),
285 schedule_code(std::move(schedule_code)),
286 schedule_type_code(std::move(schedule_type_code)),
287 valid_date(valid_date)
288{}
289
290bool operator==(const Kv1OrganizationalUnit::Key &a, const Kv1OrganizationalUnit::Key &b) {
291 return a.data_owner_code == b.data_owner_code
292 && a.organizational_unit_code == b.organizational_unit_code;
293}
294
295bool operator==(const Kv1HigherOrganizationalUnit::Key &a, const Kv1HigherOrganizationalUnit::Key &b) {
296 return a.data_owner_code == b.data_owner_code
297 && a.organizational_unit_code_parent == b.organizational_unit_code_parent
298 && a.organizational_unit_code_child == b.organizational_unit_code_child;
299}
300
301bool operator==(const Kv1UserStopPoint::Key &a, const Kv1UserStopPoint::Key &b) {
302 return a.data_owner_code == b.data_owner_code
303 && a.user_stop_code == b.user_stop_code;
304}
305
306bool operator==(const Kv1UserStopArea::Key &a, const Kv1UserStopArea::Key &b) {
307 return a.data_owner_code == b.data_owner_code
308 && a.user_stop_area_code == b.user_stop_area_code;
309}
310
311bool operator==(const Kv1TimingLink::Key &a, const Kv1TimingLink::Key &b) {
312 return a.data_owner_code == b.data_owner_code
313 && a.user_stop_code_begin == b.user_stop_code_begin
314 && a.user_stop_code_end == b.user_stop_code_end;
315}
316
317bool operator==(const Kv1Link::Key &a, const Kv1Link::Key &b) {
318 return a.data_owner_code == b.data_owner_code
319 && a.user_stop_code_begin == b.user_stop_code_begin
320 && a.user_stop_code_end == b.user_stop_code_end
321 && a.transport_type == b.transport_type;
322}
323
324bool operator==(const Kv1Line::Key &a, const Kv1Line::Key &b) {
325 return a.data_owner_code == b.data_owner_code
326 && a.line_planning_number == b.line_planning_number;
327}
328
329bool operator==(const Kv1Destination::Key &a, const Kv1Destination::Key &b) {
330 return a.data_owner_code == b.data_owner_code
331 && a.dest_code == b.dest_code;
332}
333
334bool operator==(const Kv1JourneyPattern::Key &a, const Kv1JourneyPattern::Key &b) {
335 return a.data_owner_code == b.data_owner_code
336 && a.line_planning_number == b.line_planning_number
337 && a.journey_pattern_code == b.journey_pattern_code;
338}
339
340bool operator==(const Kv1ConcessionFinancerRelation::Key &a, const Kv1ConcessionFinancerRelation::Key &b) {
341 return a.data_owner_code == b.data_owner_code
342 && a.con_fin_rel_code == b.con_fin_rel_code;
343}
344
345bool operator==(const Kv1ConcessionArea::Key &a, const Kv1ConcessionArea::Key &b) {
346 return a.data_owner_code == b.data_owner_code
347 && a.concession_area_code == b.concession_area_code;
348}
349
350bool operator==(const Kv1Financer::Key &a, const Kv1Financer::Key &b) {
351 return a.data_owner_code == b.data_owner_code
352 && a.financer_code == b.financer_code;
353}
354
355bool operator==(const Kv1JourneyPatternTimingLink::Key &a, const Kv1JourneyPatternTimingLink::Key &b) {
356 return a.data_owner_code == b.data_owner_code
357 && a.line_planning_number == b.line_planning_number
358 && a.journey_pattern_code == b.journey_pattern_code
359 && a.timing_link_order == b.timing_link_order;
360}
361
362bool operator==(const Kv1Point::Key &a, const Kv1Point::Key &b) {
363 return a.data_owner_code == b.data_owner_code
364 && a.point_code == b.point_code;
365}
366
367bool operator==(const Kv1PointOnLink::Key &a, const Kv1PointOnLink::Key &b) {
368 return a.data_owner_code == b.data_owner_code
369 && a.user_stop_code_begin == b.user_stop_code_begin
370 && a.user_stop_code_end == b.user_stop_code_end
371 && a.point_data_owner_code == b.point_data_owner_code
372 && a.point_code == b.point_code
373 && a.transport_type == b.transport_type;
374}
375
376bool operator==(const Kv1Icon::Key &a, const Kv1Icon::Key &b) {
377 return a.data_owner_code == b.data_owner_code
378 && a.icon_number == b.icon_number;
379}
380
381bool operator==(const Kv1Notice::Key &a, const Kv1Notice::Key &b) {
382 return a.data_owner_code == b.data_owner_code
383 && a.notice_code == b.notice_code;
384}
385
386bool operator==(const Kv1TimeDemandGroup::Key &a, const Kv1TimeDemandGroup::Key &b) {
387 return a.data_owner_code == b.data_owner_code
388 && a.line_planning_number == b.line_planning_number
389 && a.journey_pattern_code == b.journey_pattern_code
390 && a.time_demand_group_code == b.time_demand_group_code;
391}
392
393bool operator==(const Kv1TimeDemandGroupRunTime::Key &a, const Kv1TimeDemandGroupRunTime::Key &b) {
394 return a.data_owner_code == b.data_owner_code
395 && a.line_planning_number == b.line_planning_number
396 && a.journey_pattern_code == b.journey_pattern_code
397 && a.time_demand_group_code == b.time_demand_group_code
398 && a.timing_link_order == b.timing_link_order;
399}
400
401bool operator==(const Kv1PeriodGroup::Key &a, const Kv1PeriodGroup::Key &b) {
402 return a.data_owner_code == b.data_owner_code
403 && a.period_group_code == b.period_group_code;
404}
405
406bool operator==(const Kv1SpecificDay::Key &a, const Kv1SpecificDay::Key &b) {
407 return a.data_owner_code == b.data_owner_code
408 && a.specific_day_code == b.specific_day_code;
409}
410
411bool operator==(const Kv1TimetableVersion::Key &a, const Kv1TimetableVersion::Key &b) {
412 return a.data_owner_code == b.data_owner_code
413 && a.organizational_unit_code == b.organizational_unit_code
414 && a.timetable_version_code == b.timetable_version_code
415 && a.period_group_code == b.period_group_code
416 && a.specific_day_code == b.specific_day_code;
417}
418
419bool operator==(const Kv1PublicJourney::Key &a, const Kv1PublicJourney::Key &b) {
420 return a.data_owner_code == b.data_owner_code
421 && a.timetable_version_code == b.timetable_version_code
422 && a.organizational_unit_code == b.organizational_unit_code
423 && a.period_group_code == b.period_group_code
424 && a.specific_day_code == b.specific_day_code
425 && a.day_type == b.day_type
426 && a.line_planning_number == b.line_planning_number
427 && a.journey_number == b.journey_number;
428}
429
430bool operator==(const Kv1PeriodGroupValidity::Key &a, const Kv1PeriodGroupValidity::Key &b) {
431 return a.data_owner_code == b.data_owner_code
432 && a.organizational_unit_code == b.organizational_unit_code
433 && a.period_group_code == b.period_group_code
434 && a.valid_from == b.valid_from;
435}
436
437bool operator==(const Kv1ExceptionalOperatingDay::Key &a, const Kv1ExceptionalOperatingDay::Key &b) {
438 return a.data_owner_code == b.data_owner_code
439 && a.organizational_unit_code == b.organizational_unit_code
440 && a.valid_date == b.valid_date;
441}
442
443bool operator==(const Kv1ScheduleVersion::Key &a, const Kv1ScheduleVersion::Key &b) {
444 return a.data_owner_code == b.data_owner_code
445 && a.organizational_unit_code == b.organizational_unit_code
446 && a.schedule_code == b.schedule_code
447 && a.schedule_type_code == b.schedule_type_code;
448}
449
450bool operator==(const Kv1PublicJourneyPassingTimes::Key &a, const Kv1PublicJourneyPassingTimes::Key &b) {
451 return a.data_owner_code == b.data_owner_code
452 && a.organizational_unit_code == b.organizational_unit_code
453 && a.schedule_code == b.schedule_code
454 && a.schedule_type_code == b.schedule_type_code
455 && a.line_planning_number == b.line_planning_number
456 && a.journey_number == b.journey_number
457 && a.stop_order == b.stop_order;
458}
459
460bool operator==(const Kv1OperatingDay::Key &a, const Kv1OperatingDay::Key &b) {
461 return a.data_owner_code == b.data_owner_code
462 && a.organizational_unit_code == b.organizational_unit_code
463 && a.schedule_code == b.schedule_code
464 && a.schedule_type_code == b.schedule_type_code
465 && a.valid_date == b.valid_date;
466}
467
468namespace std::chrono {
469 static size_t hash_value(const year_month_day &ymd) {
470 size_t seed = 0;
471
472 boost::hash_combine(seed, int(ymd.year()));
473 boost::hash_combine(seed, unsigned(ymd.month()));
474 boost::hash_combine(seed, unsigned(ymd.day()));
475
476 return seed;
477 }
478
479 static size_t hash_value(const sys_seconds &s) {
480 return boost::hash<seconds::rep>()(s.time_since_epoch().count());
481 }
482}
483
484size_t hash_value(const Kv1OrganizationalUnit::Key &k) {
485 size_t seed = 0;
486
487 boost::hash_combine(seed, k.data_owner_code);
488 boost::hash_combine(seed, k.organizational_unit_code);
489
490 return seed;
491}
492
493size_t hash_value(const Kv1HigherOrganizationalUnit::Key &k) {
494 size_t seed = 0;
495
496 boost::hash_combine(seed, k.data_owner_code);
497 boost::hash_combine(seed, k.organizational_unit_code_parent);
498 boost::hash_combine(seed, k.organizational_unit_code_child);
499 boost::hash_combine(seed, k.valid_from);
500
501 return seed;
502}
503
504size_t hash_value(const Kv1UserStopPoint::Key &k) {
505 size_t seed = 0;
506
507 boost::hash_combine(seed, k.data_owner_code);
508 boost::hash_combine(seed, k.user_stop_code);
509
510 return seed;
511}
512
513size_t hash_value(const Kv1UserStopArea::Key &k) {
514 size_t seed = 0;
515
516 boost::hash_combine(seed, k.data_owner_code);
517 boost::hash_combine(seed, k.user_stop_area_code);
518
519 return seed;
520}
521
522size_t hash_value(const Kv1TimingLink::Key &k) {
523 size_t seed = 0;
524
525 boost::hash_combine(seed, k.data_owner_code);
526 boost::hash_combine(seed, k.user_stop_code_begin);
527 boost::hash_combine(seed, k.user_stop_code_end);
528
529 return seed;
530}
531
532size_t hash_value(const Kv1Link::Key &k) {
533 size_t seed = 0;
534
535 boost::hash_combine(seed, k.data_owner_code);
536 boost::hash_combine(seed, k.user_stop_code_begin);
537 boost::hash_combine(seed, k.user_stop_code_end);
538 boost::hash_combine(seed, k.transport_type);
539
540 return seed;
541}
542
543size_t hash_value(const Kv1Line::Key &k) {
544 size_t seed = 0;
545
546 boost::hash_combine(seed, k.data_owner_code);
547 boost::hash_combine(seed, k.line_planning_number);
548
549 return seed;
550}
551
552size_t hash_value(const Kv1Destination::Key &k) {
553 size_t seed = 0;
554
555 boost::hash_combine(seed, k.data_owner_code);
556 boost::hash_combine(seed, k.dest_code);
557
558 return seed;
559}
560
561size_t hash_value(const Kv1JourneyPattern::Key &k) {
562 size_t seed = 0;
563
564 boost::hash_combine(seed, k.data_owner_code);
565 boost::hash_combine(seed, k.line_planning_number);
566 boost::hash_combine(seed, k.journey_pattern_code);
567
568 return seed;
569}
570
571size_t hash_value(const Kv1ConcessionFinancerRelation::Key &k) {
572 size_t seed = 0;
573
574 boost::hash_combine(seed, k.data_owner_code);
575 boost::hash_combine(seed, k.con_fin_rel_code);
576
577 return seed;
578}
579
580size_t hash_value(const Kv1ConcessionArea::Key &k) {
581 size_t seed = 0;
582
583 boost::hash_combine(seed, k.data_owner_code);
584 boost::hash_combine(seed, k.concession_area_code);
585
586 return seed;
587}
588
589size_t hash_value(const Kv1Financer::Key &k) {
590 size_t seed = 0;
591
592 boost::hash_combine(seed, k.data_owner_code);
593 boost::hash_combine(seed, k.financer_code);
594
595 return seed;
596}
597
598size_t hash_value(const Kv1JourneyPatternTimingLink::Key &k) {
599 size_t seed = 0;
600
601 boost::hash_combine(seed, k.data_owner_code);
602 boost::hash_combine(seed, k.line_planning_number);
603 boost::hash_combine(seed, k.journey_pattern_code);
604 boost::hash_combine(seed, k.timing_link_order);
605
606 return seed;
607}
608
609size_t hash_value(const Kv1Point::Key &k) {
610 size_t seed = 0;
611
612 boost::hash_combine(seed, k.data_owner_code);
613 boost::hash_combine(seed, k.point_code);
614
615 return seed;
616}
617
618size_t hash_value(const Kv1PointOnLink::Key &k) {
619 size_t seed = 0;
620
621 boost::hash_combine(seed, k.data_owner_code);
622 boost::hash_combine(seed, k.user_stop_code_begin);
623 boost::hash_combine(seed, k.user_stop_code_end);
624 boost::hash_combine(seed, k.point_data_owner_code);
625 boost::hash_combine(seed, k.point_code);
626 boost::hash_combine(seed, k.transport_type);
627
628 return seed;
629}
630
631size_t hash_value(const Kv1Icon::Key &k) {
632 size_t seed = 0;
633
634 boost::hash_combine(seed, k.data_owner_code);
635 boost::hash_combine(seed, k.icon_number);
636
637 return seed;
638}
639
640size_t hash_value(const Kv1Notice::Key &k) {
641 size_t seed = 0;
642
643 boost::hash_combine(seed, k.data_owner_code);
644 boost::hash_combine(seed, k.notice_code);
645
646 return seed;
647}
648
649size_t hash_value(const Kv1TimeDemandGroup::Key &k) {
650 size_t seed = 0;
651
652 boost::hash_combine(seed, k.data_owner_code);
653 boost::hash_combine(seed, k.line_planning_number);
654 boost::hash_combine(seed, k.journey_pattern_code);
655 boost::hash_combine(seed, k.time_demand_group_code);
656
657 return seed;
658}
659
660size_t hash_value(const Kv1TimeDemandGroupRunTime::Key &k) {
661 size_t seed = 0;
662
663 boost::hash_combine(seed, k.data_owner_code);
664 boost::hash_combine(seed, k.line_planning_number);
665 boost::hash_combine(seed, k.journey_pattern_code);
666 boost::hash_combine(seed, k.time_demand_group_code);
667 boost::hash_combine(seed, k.timing_link_order);
668
669 return seed;
670}
671
672size_t hash_value(const Kv1PeriodGroup::Key &k) {
673 size_t seed = 0;
674
675 boost::hash_combine(seed, k.data_owner_code);
676 boost::hash_combine(seed, k.period_group_code);
677
678 return seed;
679}
680
681size_t hash_value(const Kv1SpecificDay::Key &k) {
682 size_t seed = 0;
683
684 boost::hash_combine(seed, k.data_owner_code);
685 boost::hash_combine(seed, k.specific_day_code);
686
687 return seed;
688}
689
690size_t hash_value(const Kv1TimetableVersion::Key &k) {
691 size_t seed = 0;
692
693 boost::hash_combine(seed, k.data_owner_code);
694 boost::hash_combine(seed, k.organizational_unit_code);
695 boost::hash_combine(seed, k.timetable_version_code);
696 boost::hash_combine(seed, k.period_group_code);
697 boost::hash_combine(seed, k.specific_day_code);
698
699 return seed;
700}
701
702size_t hash_value(const Kv1PublicJourney::Key &k) {
703 size_t seed = 0;
704
705 boost::hash_combine(seed, k.data_owner_code);
706 boost::hash_combine(seed, k.timetable_version_code);
707 boost::hash_combine(seed, k.organizational_unit_code);
708 boost::hash_combine(seed, k.period_group_code);
709 boost::hash_combine(seed, k.specific_day_code);
710 boost::hash_combine(seed, k.day_type);
711 boost::hash_combine(seed, k.line_planning_number);
712 boost::hash_combine(seed, k.journey_number);
713
714 return seed;
715}
716
717size_t hash_value(const Kv1PeriodGroupValidity::Key &k) {
718 size_t seed = 0;
719
720 boost::hash_combine(seed, k.data_owner_code);
721 boost::hash_combine(seed, k.organizational_unit_code);
722 boost::hash_combine(seed, k.period_group_code);
723 boost::hash_combine(seed, k.valid_from);
724
725 return seed;
726}
727
728size_t hash_value(const Kv1ExceptionalOperatingDay::Key &k) {
729 size_t seed = 0;
730
731 boost::hash_combine(seed, k.data_owner_code);
732 boost::hash_combine(seed, k.organizational_unit_code);
733 boost::hash_combine(seed, k.valid_date);
734
735 return seed;
736}
737
738size_t hash_value(const Kv1ScheduleVersion::Key &k) {
739 size_t seed = 0;
740
741 boost::hash_combine(seed, k.data_owner_code);
742 boost::hash_combine(seed, k.organizational_unit_code);
743 boost::hash_combine(seed, k.schedule_code);
744 boost::hash_combine(seed, k.schedule_type_code);
745
746 return seed;
747}
748
749size_t hash_value(const Kv1PublicJourneyPassingTimes::Key &k) {
750 size_t seed = 0;
751
752 boost::hash_combine(seed, k.data_owner_code);
753 boost::hash_combine(seed, k.organizational_unit_code);
754 boost::hash_combine(seed, k.schedule_code);
755 boost::hash_combine(seed, k.schedule_type_code);
756 boost::hash_combine(seed, k.line_planning_number);
757 boost::hash_combine(seed, k.journey_number);
758 boost::hash_combine(seed, k.stop_order);
759
760 return seed;
761}
762
763size_t hash_value(const Kv1OperatingDay::Key &k) {
764 size_t seed = 0;
765
766 boost::hash_combine(seed, k.data_owner_code);
767 boost::hash_combine(seed, k.organizational_unit_code);
768 boost::hash_combine(seed, k.schedule_code);
769 boost::hash_combine(seed, k.schedule_type_code);
770 boost::hash_combine(seed, k.valid_date);
771
772 return seed;
773}
diff --git a/lib/libtmi8/src/kv6_parquet.cpp b/lib/libtmi8/src/kv6_parquet.cpp
new file mode 100644
index 0000000..ca70b7f
--- /dev/null
+++ b/lib/libtmi8/src/kv6_parquet.cpp
@@ -0,0 +1,102 @@
1// vim:set sw=2 ts=2 sts et:
2
3#include <tmi8/kv6_parquet.hpp>
4
5ParquetBuilder::ParquetBuilder() {
6 std::shared_ptr<arrow::Field> field_type, field_data_owner_code, field_line_planning_number, field_operating_day,
7 field_journey_number, field_reinforcement_number, field_timestamp, field_source,
8 field_punctuality, field_user_stop_code, field_passage_sequence_number,
9 field_vehicle_number, field_block_code, field_wheelchair_accessible,
10 field_number_of_coaches, field_rd_y, field_rd_x, field_distance_since_last_user_stop;
11 field_type = arrow::field("type", arrow::utf8());
12 field_data_owner_code = arrow::field("data_owner_code", arrow::utf8());
13 field_line_planning_number = arrow::field("line_planning_number", arrow::utf8());
14 field_operating_day = arrow::field("operating_day", arrow::date32());
15 field_journey_number = arrow::field("journey_number", arrow::uint32());
16 field_reinforcement_number = arrow::field("reinforcement_number", arrow::uint8());
17 field_timestamp = arrow::field("timestamp", arrow::timestamp(arrow::TimeUnit::SECOND));
18 field_source = arrow::field("source", arrow::utf8());
19 field_punctuality = arrow::field("punctuality", arrow::int16());
20 field_user_stop_code = arrow::field("user_stop_code", arrow::utf8());
21 field_passage_sequence_number = arrow::field("passage_sequence_number", arrow::uint16());
22 field_vehicle_number = arrow::field("vehicle_number", arrow::uint32());
23 field_block_code = arrow::field("block_code", arrow::uint32());
24 field_wheelchair_accessible = arrow::field("wheelchair_accessible", arrow::utf8());
25 field_number_of_coaches = arrow::field("number_of_coaches", arrow::uint8());
26 field_rd_y = arrow::field("rd_y", arrow::int32());
27 field_rd_x = arrow::field("rd_x", arrow::int32());
28 field_distance_since_last_user_stop = arrow::field("distance_since_last_user_stop", arrow::uint32());
29
30 schema = arrow::schema({ field_type, field_data_owner_code, field_line_planning_number,
31 field_operating_day, field_journey_number,
32 field_reinforcement_number, field_timestamp, field_source,
33 field_punctuality, field_user_stop_code,
34 field_passage_sequence_number, field_vehicle_number,
35 field_block_code, field_wheelchair_accessible,
36 field_number_of_coaches, field_rd_y, field_rd_x,
37 field_distance_since_last_user_stop });
38}
39
40arrow::Result<std::shared_ptr<arrow::Table>> ParquetBuilder::getTable() {
41 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> types, types.Finish());
42 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> data_owner_codes, data_owner_codes.Finish());
43 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> line_planning_numbers, line_planning_numbers.Finish());
44 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> operating_days, operating_days.Finish());
45 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> journey_numbers, journey_numbers.Finish());
46 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> reinforcement_numbers, reinforcement_numbers.Finish());
47 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> timestamps, timestamps.Finish());
48 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> sources, sources.Finish());
49 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> punctualities, punctualities.Finish());
50 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> user_stop_codes, user_stop_codes.Finish());
51 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> passage_sequence_numbers, passage_sequence_numbers.Finish());
52 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> vehicle_numbers, vehicle_numbers.Finish());
53 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> block_codes, block_codes.Finish());
54 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> wheelchair_accessibles, wheelchair_accessibles.Finish());
55 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> number_of_coaches, number_of_coaches.Finish());
56 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> rd_ys, rd_ys.Finish());
57 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> rd_xs, rd_xs.Finish());
58 ARROW_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> distance_since_last_user_stops, distance_since_last_user_stops.Finish());
59
60 std::vector<std::shared_ptr<arrow::Array>> columns = { types, data_owner_codes, line_planning_numbers, operating_days,
61 journey_numbers, reinforcement_numbers, timestamps, sources,
62 punctualities, user_stop_codes, passage_sequence_numbers,
63 vehicle_numbers, block_codes, wheelchair_accessibles,
64 number_of_coaches, rd_ys, rd_xs,
65 distance_since_last_user_stops };
66 return arrow::Result(arrow::Table::Make(schema, columns));
67}
68
69arrow::Status writeArrowRecordsAsParquetFile(arrow::RecordBatchReader &rbr, std::filesystem::path filename) {
70 std::shared_ptr<parquet::WriterProperties> props = parquet::WriterProperties::Builder()
71 .compression(arrow::Compression::ZSTD)
72 ->created_by("oeuf-libtmi8")
73 ->version(parquet::ParquetVersion::PARQUET_2_6)
74 ->data_page_version(parquet::ParquetDataPageVersion::V2)
75 ->max_row_group_length(MAX_PARQUET_CHUNK)
76 ->build();
77
78 std::shared_ptr<parquet::ArrowWriterProperties> arrow_props = parquet::ArrowWriterProperties::Builder()
79 .store_schema()->build();
80
81 std::shared_ptr<arrow::io::FileOutputStream> out_file;
82 std::string filename_str = filename;
83 ARROW_ASSIGN_OR_RAISE(out_file, arrow::io::FileOutputStream::Open(filename_str + ".part"));
84
85 ARROW_ASSIGN_OR_RAISE(auto writer,
86 parquet::arrow::FileWriter::Open(*rbr.schema(), arrow::default_memory_pool(), out_file, props, arrow_props));
87 for (const auto &batchr : rbr) {
88 ARROW_ASSIGN_OR_RAISE(auto batch, batchr);
89 ARROW_RETURN_NOT_OK(writer->WriteRecordBatch(*batch));
90 }
91 ARROW_RETURN_NOT_OK(writer->Close());
92 ARROW_RETURN_NOT_OK(out_file->Close());
93
94 std::filesystem::rename(filename_str + ".part", filename);
95
96 return arrow::Status::OK();
97}
98
99arrow::Status writeArrowTableAsParquetFile(const arrow::Table &table, std::filesystem::path filename) {
100 auto tbr = arrow::TableBatchReader(table);
101 return writeArrowRecordsAsParquetFile(tbr, filename);
102}