module; #include #include module routemon:api$impl; import :api; namespace { namespace chrono = std::chrono; namespace json = boost::json; namespace views = std::views; } // namespace namespace routemon::api { auto json_value_from_point(geo::point const& p) -> json::value { return json::array{bgeo::get<1>(p), bgeo::get<0>(p)}; } auto json_value_from_linestring(geo::linestring const& ls) -> json::value { json::array a; for (auto const& p : ls) a.push_back(json_value_from_point(p)); return a; } auto json_value_from_linestrings(std::vector const& lss) -> json::value { json::array a; for (auto const& ls : lss) a.push_back(json_value_from_linestring(ls)); return a; } auto tag_invoke( json::value_from_tag, json::value& jv, relevant_road_closure const& clo) -> void { jv = json::object{ {"relevant_lss", json_value_from_linestrings(clo.relevant_lss)}, }; } auto tag_invoke( json::value_from_tag, json::value& jv, relevant_situation const& sit) -> void { jv = json::object{ {"id", json::value_from(sit.id)}, {"location", sit.location ? json_value_from_point(*sit.location) : nullptr}, {"comments", json::value_from(sit.comments)}, {"relevant_road_closures", json::value_from(sit.relevant_road_closures)}, }; } auto tag_invoke(json::value_from_tag, json::value& jv, track_segment const& seg) -> void { jv = json::object{ {"points", json_value_from_linestring(seg.points)}, }; } auto tag_invoke(json::value_from_tag, json::value& jv, track const& track) -> void { jv = json::object{ {"segments", json::value_from(track.segments)}, }; } auto tag_invoke( json::value_from_tag, json::value& jv, process_gpx_result const& res) -> void { jv = json::object{ {"tracks", json::value_from(res.tracks)}, {"relevant_situations", json::value_from(res.relevant_situations)}, }; } auto tag_invoke(json::value_from_tag, json::value& jv, sysinfo const& info) -> void { jv = json::object{ {"using_publication_of", std::format("{:%FT%TZ}", info.using_publication_of)}, {"lse_index_size", info.lse_index_size}, {"p_index_size", info.p_index_size}, }; } handler::handler(log::logger const& l, datex2::situation_publication pub) : l_{l.sub("handler")}, pub_{std::move(pub)} { l_.info("Building indices"); auto const before_build = chrono::steady_clock::now(); for (auto const& sit : pub_.situations) { for (auto const& rc : sit->road_closures) { for (auto const& ls : rc->relevant_line_strings) { auto box = geo::box{}; bgeo::envelope(*ls, box); lse_index_.insert(std::make_tuple(box, ls, rc)); } for (auto p : rc->relevant_points) { p_index_.insert(std::make_pair(p, rc)); } } } auto const after_build = chrono::steady_clock::now(); auto const dur_build = chrono::duration_cast(after_build - before_build); l_.info("Indices built in {}", dur_build); l_.info("LSE index size: {}", lse_index_.size()); l_.info("Point index size: {}", p_index_.size()); } auto handler::process_gpx(gpx::file&& gpx_file) -> std::optional { const auto min_distance = 5.0; auto const now = chrono::utc_clock::now(); auto const relevant = std::initializer_list{ time::period{now - chrono::days(7), now + chrono::days(7)} }; auto const check_periods = time::period_seq{relevant.begin(), relevant.end()}; // TODO: eliminate use of overlap segments auto splits_with_overlap_segments = std::vector{}; for (auto const& track : gpx_file.tracks) for (auto const& seg : track.segments) geo::split_linestring_with_overlap_segments( seg.waypoints, 5000 /* meters max total dist until a new split is forced */, splits_with_overlap_segments); auto const before_query = chrono::steady_clock::now(); auto vincenty_strategy = geo::vincenty_strategy{}; const auto buffer_distance = min_distance; const auto points_per_circle = 8; // Note: thomas strategy does not work for geographic_join_round; // need to use andoyer for that. using formula = bgeo::strategy::thomas; bgeo::strategy::buffer::distance_symmetric distance_strategy{buffer_distance}; bgeo::strategy::buffer::geographic_join_miter join_strategy{buffer_distance}; bgeo::strategy::buffer::geographic_end_round end_strategy{4}; bgeo::strategy::buffer::geographic_point_circle circle_strategy{points_per_circle}; bgeo::strategy::buffer::geographic_side_straight side_strategy; using polygon = bgeo::model::polygon; auto buffered_ls = bgeo::model::multi_polygon{}; l_.debug("Querying for relevant situations"); auto relevant_road_closures = std::unordered_set>{}; auto ls_checked = 0uz; auto p_checked = 0uz; auto i = 0; for (geo::linestring const& part : splits_with_overlap_segments) { l_.debug("Checking part [{}/{}]", ++i, splits_with_overlap_segments.size()); // TODO: consider buffering with min_distance auto part_box = geo::box{}; bgeo::envelope(part, part_box); for (auto it = lse_index_.qbegin(bgeo::index::intersects(part_box)); it != lse_index_.qend(); it++) { // Cannot use structured bindings here, as boost::geometry::get // interferes with ADL. It is a candidate as the namespace // boost::geometry is part of the associated namespace set, which // happens because geo::linestring ≡ // boost::geometry::model::linestring is part of the // whole tuple type (lse_index_value) that is the value_type of the // iterator. std::shared_ptr const& ls = std::get<1>(*it); std::shared_ptr const& rc = std::get<2>(*it); if (rc->validity && rc->validity->intersect(check_periods).periods().empty()) continue; bgeo::buffer(*ls, buffered_ls, distance_strategy, side_strategy, join_strategy, end_strategy, circle_strategy); if (bgeo::intersects(buffered_ls, part)) relevant_road_closures.emplace(rc); ls_checked++; } for (auto it = p_index_.qbegin(bgeo::index::intersects(part_box)); it != p_index_.qend(); it++) { // Cannot use structured bindings here for the same reason as above. geo::point const& p = std::get<0>(*it); std::shared_ptr const& rc = std::get<1>(*it); if (rc->validity && rc->validity->intersect(check_periods).periods().empty()) continue; if (bgeo::distance(p, part, vincenty_strategy) < min_distance) relevant_road_closures.emplace(rc); p_checked++; } } auto const after_query = chrono::steady_clock::now(); l_.debug( "Done (checked {} line string(s) and {} point(s)) in {}", ls_checked, p_checked, chrono::duration_cast(after_query - before_query)); auto relevant_situations = std::unordered_set>{}; for (auto const& rc : relevant_road_closures) relevant_situations.emplace(rc->parent); l_.debug( "Identified {} relevant road closure(s), part of {} unique " "situation(s)", relevant_road_closures.size(), relevant_situations.size()); for (auto const& sit : relevant_situations) l_.debug("Relevant situation: {}", sit->id); return process_gpx_result{ .tracks = gpx_file.tracks | views::transform( [](auto const& trk) -> track { return { .segments = trk.segments | views::transform( [](auto const& seg) -> track_segment { return {.points = seg.waypoints}; }) | std::ranges::to>(), }; }) | std::ranges::to>(), .relevant_situations = relevant_situations | views::transform( [&](std::shared_ptr sit) -> relevant_situation { return { .id = sit->id, .location = sit->location, .comments = sit->comments, .relevant_road_closures = relevant_road_closures | views::filter( [&](std::shared_ptr const& rc) -> bool { return std::shared_ptr{rc->parent} == sit; }) | views::transform( [](std::shared_ptr const& rc) -> relevant_road_closure { return { .relevant_lss = rc->relevant_line_strings | views::transform( [](auto const& lsp) -> geo::linestring { return *lsp; }) | std::ranges:: to>(), }; }) | std::ranges::to>(), }; }) | std::ranges::to>(), }; } auto handler::sysinfo() -> struct sysinfo { return { .using_publication_of = pub_.publication_time, .lse_index_size = lse_index_.size(), .p_index_size = p_index_.size(), }; } } // namespace routemon::api