diff options
Diffstat (limited to 'server/src/api.cpp')
| -rw-r--r-- | server/src/api.cpp | 206 |
1 files changed, 206 insertions, 0 deletions
diff --git a/server/src/api.cpp b/server/src/api.cpp new file mode 100644 index 0000000..b3c8e31 --- /dev/null +++ b/server/src/api.cpp | |||
| @@ -0,0 +1,206 @@ | |||
| 1 | module; | ||
| 2 | |||
| 3 | #include <boost/geometry.hpp> | ||
| 4 | #include <boost/json.hpp> | ||
| 5 | |||
| 6 | module routemon:api$impl; | ||
| 7 | |||
| 8 | import std; | ||
| 9 | import :api; | ||
| 10 | import :datex2; | ||
| 11 | import :geo; | ||
| 12 | import :gpx; | ||
| 13 | import :log; | ||
| 14 | import :req_ctx; | ||
| 15 | import :time; | ||
| 16 | import :trace; | ||
| 17 | |||
| 18 | namespace { | ||
| 19 | |||
| 20 | namespace chrono = std::chrono; | ||
| 21 | namespace json = boost::json; | ||
| 22 | namespace views = std::views; | ||
| 23 | |||
| 24 | } // namespace <anonymous> | ||
| 25 | |||
| 26 | namespace routemon::api { | ||
| 27 | |||
| 28 | auto json_value_from_point(geo::point const& p) -> json::value { | ||
| 29 | return json::array{bgeo::get<1>(p), bgeo::get<0>(p)}; | ||
| 30 | } | ||
| 31 | auto json_value_from_linestring(geo::linestring const& ls) -> json::value { | ||
| 32 | json::array a; | ||
| 33 | for (auto const& p : ls) | ||
| 34 | a.push_back(json_value_from_point(p)); | ||
| 35 | return a; | ||
| 36 | } | ||
| 37 | auto json_value_from_linestrings(std::vector<geo::linestring> const& lss) -> json::value { | ||
| 38 | json::array a; | ||
| 39 | for (auto const& ls : lss) | ||
| 40 | a.push_back(json_value_from_linestring(ls)); | ||
| 41 | return a; | ||
| 42 | } | ||
| 43 | |||
| 44 | auto tag_invoke(json::value_from_tag, json::value& jv, relevant_road_closure const& clo) -> void { | ||
| 45 | jv = json::object{ | ||
| 46 | {"relevant_lss", json_value_from_linestrings(clo.relevant_lss)}, | ||
| 47 | }; | ||
| 48 | } | ||
| 49 | auto tag_invoke(json::value_from_tag, json::value& jv, relevant_situation const& sit) -> void { | ||
| 50 | jv = json::object{ | ||
| 51 | {"id", json::value_from(sit.id)}, | ||
| 52 | {"location", sit.location ? json_value_from_point(*sit.location) : nullptr}, | ||
| 53 | {"comments", json::value_from(sit.comments)}, | ||
| 54 | {"relevant_road_closures", json::value_from(sit.relevant_road_closures)}, | ||
| 55 | }; | ||
| 56 | } | ||
| 57 | auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) -> void { | ||
| 58 | jv = json::object{ | ||
| 59 | {"points", json_value_from_linestring(seg.points)}, | ||
| 60 | }; | ||
| 61 | } | ||
| 62 | auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) -> void { | ||
| 63 | jv = json::object{ | ||
| 64 | {"segments", json::value_from(track.segments)}, | ||
| 65 | }; | ||
| 66 | } | ||
| 67 | auto tag_invoke(json::value_from_tag, json::value& jv, process_gpx_result const& res) -> void { | ||
| 68 | jv = json::object{ | ||
| 69 | {"tracks", json::value_from(res.tracks)}, | ||
| 70 | {"relevant_situations", json::value_from(res.relevant_situations)}, | ||
| 71 | }; | ||
| 72 | } | ||
| 73 | auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) -> void { | ||
| 74 | jv = json::object{ | ||
| 75 | {"using_publication_of", std::format("{:%FT%TZ}", info.using_publication_of)}, | ||
| 76 | }; | ||
| 77 | } | ||
| 78 | |||
| 79 | handler::handler(log::logger const& l, datex2::situation_publication pub) | ||
| 80 | : l_{l.sub("handler")}, pub_{std::move(pub)} | ||
| 81 | { | ||
| 82 | l_.info("Building indices"); | ||
| 83 | auto const before_build = chrono::steady_clock::now(); | ||
| 84 | for (auto const& sit : pub_.situations) { | ||
| 85 | for (auto const& rc : sit->road_closures) { | ||
| 86 | for (auto const& ls : rc->relevant_line_strings) { | ||
| 87 | auto box = geo::box{}; | ||
| 88 | bgeo::envelope(*ls, box); | ||
| 89 | lse_index_.insert(std::make_tuple(box, ls, rc)); | ||
| 90 | } | ||
| 91 | for (auto p : rc->relevant_points) { | ||
| 92 | p_index_.insert(std::make_pair(p, rc)); | ||
| 93 | } | ||
| 94 | } | ||
| 95 | } | ||
| 96 | auto const after_build = chrono::steady_clock::now(); | ||
| 97 | auto const dur_build = chrono::duration_cast<chrono::milliseconds>(after_build - before_build); | ||
| 98 | l_.info("Indices built in {}", dur_build); | ||
| 99 | l_.info("LSE index size: {}", lse_index_.size()); | ||
| 100 | l_.info("Point index size: {}", p_index_.size()); | ||
| 101 | } | ||
| 102 | |||
| 103 | auto handler::process_gpx(gpx::file&& gpx_file) -> std::optional<process_gpx_result> { | ||
| 104 | auto const now = chrono::utc_clock::now(); | ||
| 105 | auto const relevant = std::initializer_list<time::period>{time::period{now - chrono::days(7), now + chrono::days(7)}}; | ||
| 106 | auto const check_periods = time::period_seq{relevant.begin(), relevant.end()}; | ||
| 107 | |||
| 108 | auto splits_with_overlap_segments = std::vector<geo::linestring>{}; | ||
| 109 | for (auto const& track : gpx_file.tracks) | ||
| 110 | for (auto const& seg : track.segments) | ||
| 111 | geo::split_linestring_with_overlap_segments(seg.waypoints, 5000 /* meters max total dist until a new split is forced */, | ||
| 112 | splits_with_overlap_segments); | ||
| 113 | auto const before_query = chrono::steady_clock::now(); | ||
| 114 | |||
| 115 | l_.debug("Querying for relevant situations"); | ||
| 116 | auto relevant_road_closures = std::unordered_set<std::shared_ptr<datex2::road_closure>>{}; | ||
| 117 | auto ls_checked = 0uz; | ||
| 118 | auto p_checked = 0uz; | ||
| 119 | auto i = 0; | ||
| 120 | for (geo::linestring const& part : splits_with_overlap_segments) { | ||
| 121 | l_.debug("Checking part [{}/{}]", ++i, splits_with_overlap_segments.size()); | ||
| 122 | |||
| 123 | auto part_box = geo::box{}; | ||
| 124 | bgeo::envelope(part, part_box); | ||
| 125 | |||
| 126 | for (auto it = lse_index_.qbegin(bgeo::index::intersects(part_box)); it != lse_index_.qend(); it++) { | ||
| 127 | // Cannot use structured bindings here, as boost::geometry::get interferes with ADL. | ||
| 128 | // It is a candidate as the namespace boost::geometry is part of the associated namespace set, | ||
| 129 | // which happens because geo::linestring ≡ boost::geometry::model::linestring<geo::point> is part | ||
| 130 | // of the whole tuple type (lse_index_value) that is the value_type of the iterator. | ||
| 131 | std::shared_ptr<geo::linestring> const& ls = std::get<1>(*it); | ||
| 132 | std::shared_ptr<datex2::road_closure> const& rc = std::get<2>(*it); | ||
| 133 | if (rc->validity && rc->validity->intersect(check_periods).periods().empty()) | ||
| 134 | continue; | ||
| 135 | if (bgeo::distance(*ls, part, geo::vincenty_strategy{}) < 5.0) | ||
| 136 | relevant_road_closures.emplace(rc); | ||
| 137 | ls_checked++; | ||
| 138 | } | ||
| 139 | for (auto it = p_index_.qbegin(bgeo::index::intersects(part_box)); it != p_index_.qend(); it++) { | ||
| 140 | // Cannot use structured bindings here for the same reason as above. | ||
| 141 | geo::point const& p = std::get<0>(*it); | ||
| 142 | std::shared_ptr<datex2::road_closure> const& rc = std::get<1>(*it); | ||
| 143 | if (rc->validity && rc->validity->intersect(check_periods).periods().empty()) | ||
| 144 | continue; | ||
| 145 | if (bgeo::distance(p, part, geo::vincenty_strategy{}) < 5.0) | ||
| 146 | relevant_road_closures.emplace(rc); | ||
| 147 | p_checked++; | ||
| 148 | } | ||
| 149 | } | ||
| 150 | |||
| 151 | auto const after_query = chrono::steady_clock::now(); | ||
| 152 | l_.debug("Done (checked {} line string(s) and {} point(s)) in {}", | ||
| 153 | ls_checked, p_checked, chrono::duration_cast<chrono::milliseconds>(after_query - before_query)); | ||
| 154 | |||
| 155 | auto relevant_situations = std::unordered_set<std::shared_ptr<datex2::situation>>{}; | ||
| 156 | for (auto const& rc : relevant_road_closures) | ||
| 157 | relevant_situations.emplace(rc->parent); | ||
| 158 | |||
| 159 | l_.debug("Identified {} relevant road closure(s), part of {} unique situation(s)", | ||
| 160 | relevant_road_closures.size(), relevant_situations.size()); | ||
| 161 | for (auto const& sit : relevant_situations) | ||
| 162 | l_.debug("Relevant situation: {}", sit->id); | ||
| 163 | |||
| 164 | return process_gpx_result{ | ||
| 165 | .tracks = gpx_file.tracks | ||
| 166 | | views::transform([](auto const& trk) -> track { | ||
| 167 | return { | ||
| 168 | .segments = trk.segments | ||
| 169 | | views::transform([](auto const& seg) -> track_segment { | ||
| 170 | return {.points = seg.waypoints}; | ||
| 171 | }) | ||
| 172 | | std::ranges::to<std::vector<track_segment>>(), | ||
| 173 | }; | ||
| 174 | }) | ||
| 175 | | std::ranges::to<std::vector<track>>(), | ||
| 176 | .relevant_situations = relevant_situations | ||
| 177 | | views::transform([&](std::shared_ptr<datex2::situation> sit) -> relevant_situation { | ||
| 178 | return { | ||
| 179 | .id = sit->id, | ||
| 180 | .location = sit->location, | ||
| 181 | .comments = sit->comments, | ||
| 182 | .relevant_road_closures = relevant_road_closures | ||
| 183 | | views::filter([&](std::shared_ptr<datex2::road_closure> const& rc) -> bool { | ||
| 184 | return std::shared_ptr{rc->parent} == sit; | ||
| 185 | }) | ||
| 186 | | views::transform([](std::shared_ptr<datex2::road_closure> const& rc) -> relevant_road_closure { | ||
| 187 | return { | ||
| 188 | .relevant_lss = rc->relevant_line_strings | ||
| 189 | | views::transform([](auto const& lsp) -> geo::linestring { | ||
| 190 | return *lsp; | ||
| 191 | }) | ||
| 192 | | std::ranges::to<std::vector<geo::linestring>>(), | ||
| 193 | }; | ||
| 194 | }) | ||
| 195 | | std::ranges::to<std::vector<relevant_road_closure>>(), | ||
| 196 | }; | ||
| 197 | }) | ||
| 198 | | std::ranges::to<std::vector<relevant_situation>>(), | ||
| 199 | }; | ||
| 200 | } | ||
| 201 | |||
| 202 | auto handler::sysinfo() -> struct sysinfo { | ||
| 203 | return {.using_publication_of = pub_.publication_time}; | ||
| 204 | } | ||
| 205 | |||
| 206 | } // namespace routemon::api | ||