summaryrefslogtreecommitdiffstats
path: root/server/src/datex2.cpp
diff options
context:
space:
mode:
authorRutger Broekhoff2026-08-29 12:01:42 +0200
committerRutger Broekhoff2026-08-29 12:01:42 +0200
commit52b3cd46c76fd4be18aeb8422a08a073484b9fad (patch)
treeb4f23957c096e6bce5369bb94a4e8e23de71761a /server/src/datex2.cpp
parent35878b76049b9c3e752480e9f298b7e2da6fdf1f (diff)
downloadroutemon-52b3cd46c76fd4be18aeb8422a08a073484b9fad.tar.gz
routemon-52b3cd46c76fd4be18aeb8422a08a073484b9fad.zip
More module implementation partition units
Diffstat (limited to 'server/src/datex2.cpp')
-rw-r--r--server/src/datex2.cpp377
1 files changed, 377 insertions, 0 deletions
diff --git a/server/src/datex2.cpp b/server/src/datex2.cpp
new file mode 100644
index 0000000..4c8000b
--- /dev/null
+++ b/server/src/datex2.cpp
@@ -0,0 +1,377 @@
1module;
2
3#include <boost/geometry/algorithms/is_empty.hpp>
4#include <boost/geometry/srs/epsg.hpp>
5#include <boost/geometry/srs/transformation.hpp>
6
7#include <pugixml.hpp>
8
9module routemon:datex2$impl;
10
11import :datex2;
12
13using namespace std::literals::string_view_literals;
14
15namespace routemon::datex2 {
16
17auto parse_timestamp(char const* in) -> std::optional<time::timestamp>
18{
19 auto res = time::timestamp{};
20 auto is = std::istringstream{in};
21 is >> std::chrono::parse("%Y-%m-%dT%H:%M:%SZ", res);
22 return is.fail() ? std::nullopt : std::make_optional(res);
23}
24
25auto loader::add_location_from_xml(
26 road_closure& rc, pugi::xml_node const& loc_xml) -> void
27{
28 auto loc_xml_type = std::string_view{loc_xml.attribute("xsi:type").value()};
29 if (loc_xml_type == "loc:ItineraryByIndexedLocations")
30 {
31 for (auto const loc_cont_xml :
32 loc_xml.children("loc:locationContainedInItinerary"))
33 {
34 add_location_from_xml(rc, loc_cont_xml.child("loc:location"));
35 }
36 }
37 else if (loc_xml_type == "loc:LinearLocation"
38 || loc_xml_type == "loc:SingleRoadLinearLocation")
39 {
40 auto const& loc_gml_xml = loc_xml.child("loc:gmlLineString");
41 if (!loc_gml_xml)
42 return;
43
44 auto const srs_name =
45 std::string_view{loc_gml_xml.attribute("srsName").value()};
46 if (srs_name != "WGS 84"sv)
47 {
48 warnings_.insert(
49 std::format("don't now how to handle the CRS {}", srs_name));
50 return;
51 }
52 auto const pos_list_str =
53 std::string_view{loc_gml_xml.child_value("loc:posList")};
54 // lat1 long1 lat2 long2 ... lat(n-1) long(n-1) latn longn
55
56 auto ls = std::make_shared<geo::linestring>();
57
58 auto lat_set = false;
59 auto lat = 0.0;
60 for (auto const lat_or_long_str : std::views::split(pos_list_str, " "sv))
61 {
62 auto mlat_or_long = util::parse_double(std::string_view{lat_or_long_str});
63 if (!mlat_or_long)
64 {
65 warnings_.insert(
66 std::format(
67 "failed to parse coordinate {:?}",
68 std::string_view{lat_or_long_str}));
69 return;
70 }
71
72 if (!lat_set)
73 {
74 lat = *mlat_or_long;
75 lat_set = true;
76 }
77 else
78 {
79 bgeo::append(*ls, geo::point{*mlat_or_long, lat});
80 lat = 0;
81 lat_set = false;
82 }
83 }
84
85 if (bgeo::is_empty(*ls))
86 {
87 warnings_.emplace("empty line string in data set");
88 return;
89 }
90
91 rc.relevant_line_strings.push_back(ls);
92 }
93 else if (loc_xml_type == "loc:PointLocation")
94 {
95 auto const& coords_xml =
96 loc_xml.child("loc:pointByCoordinates").child("loc:pointCoordinates");
97 if (!coords_xml)
98 return;
99
100 auto mlat = util::parse_double(coords_xml.child_value("loc:latitude"));
101 auto mlon = util::parse_double(coords_xml.child_value("loc:longitude"));
102 if (!mlat || !mlon)
103 {
104 warnings_.emplace("failed to parse PointLocation coordinates");
105 return;
106 }
107
108 // Vaag genoeg zegt NDW dat het hier om WGS 84 gaat:
109 // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/
110 // maar heeft het UML-model van DATEX II v3 het over ETRS 89:
111 // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm
112
113 auto const coords_etrs89 = geo::point{*mlon, *mlat};
114 auto coords_wgs84 = geo::point{};
115 etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84);
116
117 rc.relevant_points.push_back(coords_wgs84);
118 }
119 else
120 {
121 warnings_.insert(
122 std::format(
123 "don't know how to hande location of type {}, ignoring",
124 loc_xml.attribute("xsi:type").value()));
125 return;
126 }
127}
128
129auto loader::handle_road_or_carriageway_or_lane_management(
130 pugi::xml_node const& record_xml, std::weak_ptr<situation> parent)
131 -> std::optional<std::shared_ptr<road_closure>>
132{
133 auto const type = std::string_view{
134 record_xml.child("sit:roadOrCarriagewayOrLaneManagementType").child_value()
135 };
136 if (type != "carriagewayClosures" && type != "roadClosed")
137 // TODO: checken of er nog andere types fietsers de doorgang zouden
138 // kunnen blokkeren?
139 return std::nullopt;
140
141 auto const& restricted_vehicle_types_xml =
142 record_xml.child("sit:forVehiclesWithCharacteristicsOf");
143 bool likely_restriction_for_bikes = restricted_vehicle_types_xml.empty();
144 for (auto const vehicle_type_xml :
145 restricted_vehicle_types_xml.children("com:vehicleType"))
146 {
147 auto vehicle_type = std::string_view{vehicle_type_xml.child_value()};
148 if (vehicle_type == "anyVehicle" || vehicle_type == "bicycle"
149 || vehicle_type == "unknown" || vehicle_type == "other")
150 {
151 likely_restriction_for_bikes = true;
152 }
153 }
154 if (!likely_restriction_for_bikes)
155 return std::nullopt;
156
157 //---- Check if within the defined validity period
158
159 auto validity = std::optional<time::period_seq>{};
160 auto const& validity_xml = record_xml.child("sit:validity");
161 if (validity_xml
162 && validity_xml.child_value("com:validityStatus")
163 == "definedByValidityTimeSpec"sv)
164 {
165 auto const& validity_spec_xml =
166 validity_xml.child("com:validityTimeSpecification");
167
168 auto valid_periods = std::vector<time::period>{};
169 auto exception_periods = std::vector<time::period>{};
170
171 // TODO: com:overallEndTime may be missing (according to the DATEX
172 // II v3 data model)
173 auto const overall_start_time =
174 parse_timestamp(validity_spec_xml.child_value("com:overallStartTime"));
175 auto const overall_end_time =
176 parse_timestamp(validity_spec_xml.child_value("com:overallEndTime"));
177 if (overall_start_time && overall_end_time
178 && *overall_start_time < *overall_end_time)
179 {
180 valid_periods.emplace_back(*overall_start_time, *overall_end_time);
181
182 for (auto const valid_period_xml :
183 validity_xml.children("com:validPeriod"))
184 {
185 auto const start_of_period =
186 parse_timestamp(valid_period_xml.child_value("com:startOfPeriod"));
187 auto const end_of_period =
188 parse_timestamp(valid_period_xml.child_value("com:endOfPeriod"));
189 if (start_of_period && end_of_period
190 && *start_of_period < *end_of_period)
191 {
192 valid_periods.emplace_back(*start_of_period, *end_of_period);
193 }
194 }
195 for (auto const exception_period_xml :
196 validity_xml.children("com:exceptionPeriod"))
197 {
198 auto const start_of_period = parse_timestamp(
199 exception_period_xml.child_value("com:startOfPeriod"));
200 auto const end_of_period = parse_timestamp(
201 exception_period_xml.child_value("com:endOfPeriod"));
202 if (start_of_period && end_of_period
203 && *start_of_period < *end_of_period)
204 {
205 exception_periods.emplace_back(*start_of_period, *end_of_period);
206 }
207 }
208
209 validity =
210 time::period_seq{valid_periods.begin(), valid_periods.end()}.except(
211 time::period_seq{
212 exception_periods.begin(), exception_periods.end()
213 });
214 }
215 else
216 {
217 warnings_.insert(
218 std::format(
219 "invalid overall start / end time (start time: {}, end "
220 "time: {})",
221 validity_spec_xml.child_value("com:overallStartTime"),
222 validity_spec_xml.child_value("com:overallEndTime")));
223 return std::nullopt;
224 }
225 }
226
227 //---- Try to extract the location info
228
229 auto rc = std::make_shared<road_closure>(std::move(parent), validity);
230 add_location_from_xml(*rc, record_xml.child("sit:locationReference"));
231 return rc;
232}
233
234auto loader::load_situation_publication(std::string const& filename)
235 -> situation_publication
236{
237 auto doc = pugi::xml_document{};
238 if (auto result = doc.load_file(filename.c_str()); !result)
239 {
240 throw std::runtime_error{result.description()};
241 }
242 auto payload_xml = doc.child("mc:messageContainer").child("mc:payload");
243 auto mpublication_time =
244 parse_timestamp(payload_xml.child_value("com:publicationTime"));
245 if (!mpublication_time)
246 throw std::runtime_error{
247 "provided publication does not name publication time"
248 };
249
250 auto situations = std::vector<std::shared_ptr<situation>>{};
251 for (auto const sit_xml : payload_xml.children("sit:situation"))
252 {
253 auto id = std::string_view{sit_xml.attribute("id").value()};
254
255 auto const sit = std::make_shared<situation>(std::string{id});
256 situations.push_back(sit);
257
258 auto const& header_info_xml = sit_xml.child("sit:headerInformation");
259 if (header_info_xml.child_value("com:informationStatus") != "real"sv)
260 continue;
261
262 for (auto const record_xml : sit_xml.children("sit:situationRecord"))
263 {
264 auto const record_type =
265 std::string_view{record_xml.attribute("xsi:type").value()};
266 auto const primary_record_types = std::unordered_set<std::string_view>{
267 "sit:Roadworks",
268 /* { */ "sit:MaintenanceWorks",
269 /* | */ "sit:ConstructionWorks",
270 /* } */
271 "sit:Obstruction",
272 /* { */ "sit:EnvironmentalObstruction",
273 /* | */ "sit:GeneralObstruction",
274 /* | */ "sit:InfrastructureDamageObstruction",
275 /* } */
276 "sit:Activity",
277 /* { */ "sit:PublicEvent",
278 /* } */
279 };
280
281 if (record_type == "sit:RoadOrCarriagewayOrLaneManagement")
282 {
283 if (auto rc =
284 handle_road_or_carriageway_or_lane_management(record_xml, sit))
285 {
286 sit->road_closures.push_back(*rc);
287 }
288 }
289 else if (primary_record_types.contains(record_type))
290 {
291 for (auto const comment_xml :
292 record_xml.children("sit:generalPublicComment"))
293 {
294 // if
295 // (comment_xml.child_value("sit:commentType")
296 // == "internalNote"sv) {
297 auto candidate = std::optional<
298 std::pair<std::string_view, std::string_view>
299 >{}; // (text, language)
300 for (auto const comment_value_xml : comment_xml.child("sit:comment")
301 .child("com:values")
302 .children("com:value"))
303 {
304 if (!candidate
305 || comment_value_xml.attribute("lang").value() == "nl"sv
306 || (candidate->second != "nl"sv
307 && comment_value_xml.attribute("lang").value() == "nl"sv))
308 {
309 candidate = std::make_pair(
310 comment_value_xml.child_value(),
311 comment_value_xml.attribute("lang").value());
312 }
313 }
314 if (candidate)
315 {
316 auto already_present = false;
317 for (auto const& comment : sit->comments)
318 already_present = already_present || comment == candidate->first;
319 if (!already_present)
320 {
321 sit->comments.emplace_back(candidate->first);
322 }
323 }
324 // }
325 }
326
327 if (auto const location_ref_xml =
328 record_xml.child("sit:locationReference"))
329 {
330 if (location_ref_xml.attribute("xsi:type").value()
331 == "loc:PointLocation"sv)
332 {
333 if (auto const coords_xml =
334 location_ref_xml.child("loc:pointByCoordinates")
335 .child("loc:pointCoordinates"))
336 {
337 auto const mlat =
338 util::parse_double(coords_xml.child_value("loc:latitude"));
339 auto const mlon =
340 util::parse_double(coords_xml.child_value("loc:longitude"));
341 if (mlat && mlon)
342 {
343 // Vaag genoeg zegt NDW dat het hier om WGS
344 // 84 gaat:
345 // https://docs.ndw.nu/en/dataformaten/datex2-v3/elementen/locationreferencing/pointCoordinates/
346 // maar heeft het UML-model van DATEX II v3
347 // het over ETRS 89:
348 // https://docs.datex2.eu/_static/data/v3.7/umlmodel/html/EARoot/EA3/EA3/EA5/EA676.htm
349
350 auto const coords_etrs89 = geo::point{*mlon, *mlat};
351 auto coords_wgs84 = geo::point{};
352 etrs89_to_wgs84_.forward(coords_etrs89, coords_wgs84);
353
354 if (!sit->location)
355 {
356 sit->location = coords_wgs84;
357 }
358 }
359 }
360 }
361 }
362 }
363 }
364 }
365
366 return {
367 .publication_time = *mpublication_time,
368 .situations = situations,
369 };
370}
371
372auto loader::warnings() const -> std::multiset<std::string> const&
373{
374 return warnings_;
375}
376
377} // namespace routemon::datex2