Add trip plugin
This commit is contained in:
		
							parent
							
								
									b34f9b1795
								
							
						
					
					
						commit
						8378d95588
					
				
							
								
								
									
										110
									
								
								include/engine/api/trip_api.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								include/engine/api/trip_api.hpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,110 @@ | ||||
| #ifndef ENGINE_API_TRIP_HPP | ||||
| #define ENGINE_API_TRIP_HPP | ||||
| 
 | ||||
| #include "engine/api/route_api.hpp" | ||||
| #include "engine/api/trip_parameters.hpp" | ||||
| 
 | ||||
| #include "engine/datafacade/datafacade_base.hpp" | ||||
| 
 | ||||
| #include "engine/internal_route_result.hpp" | ||||
| 
 | ||||
| #include "util/integer_range.hpp" | ||||
| 
 | ||||
| namespace osrm | ||||
| { | ||||
| namespace engine | ||||
| { | ||||
| namespace api | ||||
| { | ||||
| 
 | ||||
| class TripAPI final : public RouteAPI | ||||
| { | ||||
|   public: | ||||
|     TripAPI(const datafacade::BaseDataFacade &facade_, const TripParameters ¶meters_) | ||||
|         : RouteAPI(facade_, parameters_), parameters(parameters_) | ||||
|     { | ||||
|     } | ||||
| 
 | ||||
|     void MakeResponse(const std::vector<std::vector<NodeID>> &sub_trips, | ||||
|                       const std::vector<InternalRouteResult> &sub_routes, | ||||
|                       const std::vector<PhantomNode> &phantoms, | ||||
|                       util::json::Object &response) const | ||||
|     { | ||||
|         auto number_of_routes = sub_trips.size(); | ||||
|         util::json::Array routes; | ||||
|         routes.values.reserve(number_of_routes); | ||||
|         BOOST_ASSERT(sub_trips.size() == sub_routes.size()); | ||||
|         for (auto index : util::irange<std::size_t>(0UL, sub_trips.size())) | ||||
|         { | ||||
|             auto route = MakeRoute(sub_routes[index].segment_end_coordinates, | ||||
|                                    sub_routes[index].unpacked_path_segments, | ||||
|                                    sub_routes[index].source_traversed_in_reverse, | ||||
|                                    sub_routes[index].target_traversed_in_reverse); | ||||
|             routes.values.push_back(std::move(route)); | ||||
|         } | ||||
|         response.values["waypoints"] = MakeWaypoints(sub_trips, phantoms); | ||||
|         response.values["routes"] = std::move(routes); | ||||
|         response.values["code"] = "ok"; | ||||
|     } | ||||
| 
 | ||||
|   protected: | ||||
|     // FIXME this logic is a little backwards. We should change the output format of the
 | ||||
|     // trip plugin routing algorithm to be easier to consume here.
 | ||||
|     util::json::Array MakeWaypoints(const std::vector<std::vector<NodeID>> &sub_trips, | ||||
|                                     const std::vector<PhantomNode> &phantoms) const | ||||
|     { | ||||
|         util::json::Array waypoints; | ||||
|         waypoints.values.reserve(parameters.coordinates.size()); | ||||
| 
 | ||||
|         struct TripIndex | ||||
|         { | ||||
|             TripIndex() = default; | ||||
|             TripIndex(unsigned sub_trip_index_, unsigned point_index_) | ||||
|                 : sub_trip_index(sub_trip_index_), point_index(point_index_) | ||||
|             { | ||||
|             } | ||||
| 
 | ||||
|             unsigned sub_trip_index = std::numeric_limits<unsigned>::max(); | ||||
|             unsigned point_index = std::numeric_limits<unsigned>::max(); | ||||
| 
 | ||||
|             bool NotUsed() | ||||
|             { | ||||
|                 return sub_trip_index == std::numeric_limits<unsigned>::max() && | ||||
|                        point_index == std::numeric_limits<unsigned>::max(); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         std::vector<TripIndex> input_idx_to_trip_idx(parameters.coordinates.size()); | ||||
|         for (auto sub_trip_index : util::irange(0u, static_cast<unsigned>(sub_trips.size()))) | ||||
|         { | ||||
|             for (auto point_index : | ||||
|                  util::irange(0u, static_cast<unsigned>(sub_trips[sub_trip_index].size()))) | ||||
|             { | ||||
|                 input_idx_to_trip_idx[sub_trips[sub_trip_index][point_index]] = | ||||
|                     TripIndex{sub_trip_index, point_index}; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         for (auto input_index : util::irange(0UL, parameters.coordinates.size())) | ||||
|         { | ||||
|             auto trip_index = input_idx_to_trip_idx[input_index]; | ||||
|             BOOST_ASSERT(!trip_index.NotUsed()); | ||||
| 
 | ||||
|             auto waypoint = | ||||
|                 BaseAPI::MakeWaypoint(parameters.coordinates[input_index], phantoms[input_index]); | ||||
|             waypoint.values["trips_index"] = trip_index.sub_trip_index; | ||||
|             waypoint.values["waypoint_index"] = trip_index.point_index; | ||||
|             waypoints.values.push_back(std::move(waypoint)); | ||||
|         } | ||||
| 
 | ||||
|         return waypoints; | ||||
|     } | ||||
| 
 | ||||
|     const TripParameters ¶meters; | ||||
| }; | ||||
| 
 | ||||
| } // ns api
 | ||||
| } // ns engine
 | ||||
| } // ns osrm
 | ||||
| 
 | ||||
| #endif | ||||
| @ -14,7 +14,7 @@ namespace api | ||||
| 
 | ||||
| struct TripParameters : public RouteParameters | ||||
| { | ||||
|     bool IsValid() const; | ||||
|     //bool IsValid() const; Falls back to base class
 | ||||
| }; | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -73,7 +73,7 @@ class Engine final | ||||
|     std::unique_ptr<plugins::ViaRoutePlugin> route_plugin; | ||||
|     std::unique_ptr<plugins::TablePlugin> table_plugin; | ||||
|     std::unique_ptr<plugins::NearestPlugin> nearest_plugin; | ||||
|     // std::unique_ptr<plugins::TripPlugin> trip_plugin;
 | ||||
|     std::unique_ptr<plugins::TripPlugin> trip_plugin; | ||||
|     std::unique_ptr<plugins::MatchPlugin> match_plugin; | ||||
| 
 | ||||
|     std::unique_ptr<datafacade::BaseDataFacade> query_data_facade; | ||||
|  | ||||
| @ -3,16 +3,10 @@ | ||||
| 
 | ||||
| #include "engine/plugins/plugin_base.hpp" | ||||
| 
 | ||||
| #include "engine/object_encoder.hpp" | ||||
| #include "extractor/tarjan_scc.hpp" | ||||
| #include "engine/trip/trip_nearest_neighbour.hpp" | ||||
| #include "engine/trip/trip_farthest_insertion.hpp" | ||||
| #include "engine/trip/trip_brute_force.hpp" | ||||
| #include "engine/search_engine.hpp" | ||||
| #include "util/matrix_graph_wrapper.hpp" // wrapper to use tarjan scc on dist table
 | ||||
| #include "engine/api_response_generator.hpp" | ||||
| #include "util/make_unique.hpp" | ||||
| #include "util/dist_table_wrapper.hpp" // to access the dist table more easily
 | ||||
| #include "engine/api/trip_parameters.hpp" | ||||
| #include "engine/routing_algorithms/shortest_path.hpp" | ||||
| #include "engine/routing_algorithms/many_to_many.hpp" | ||||
| 
 | ||||
| #include "osrm/json_container.hpp" | ||||
| 
 | ||||
| #include <boost/assert.hpp> | ||||
| @ -32,335 +26,28 @@ namespace engine | ||||
| namespace plugins | ||||
| { | ||||
| 
 | ||||
| template <class DataFacadeT> class RoundTripPlugin final : public BasePlugin | ||||
| class TripPlugin final : public BasePlugin | ||||
| { | ||||
|   private: | ||||
|     std::string descriptor_string; | ||||
|     DataFacadeT *facade; | ||||
|     std::unique_ptr<SearchEngine<DataFacadeT>> search_engine_ptr; | ||||
|     SearchEngineData heaps; | ||||
|     routing_algorithms::ShortestPathRouting<datafacade::BaseDataFacade> shortest_path; | ||||
|     routing_algorithms::ManyToManyRouting<datafacade::BaseDataFacade> duration_table; | ||||
|     int max_locations_trip; | ||||
| 
 | ||||
|   public: | ||||
|     explicit RoundTripPlugin(DataFacadeT *facade, int max_locations_trip) | ||||
|         : descriptor_string("trip"), facade(facade), max_locations_trip(max_locations_trip) | ||||
|     { | ||||
|         search_engine_ptr = util::make_unique<SearchEngine<DataFacadeT>>(facade); | ||||
|     } | ||||
| 
 | ||||
|     const std::string GetDescriptor() const override final { return descriptor_string; } | ||||
| 
 | ||||
|     std::vector<PhantomNode> GetPhantomNodes(const RouteParameters &route_parameters) | ||||
|     { | ||||
|         const bool checksum_OK = (route_parameters.check_sum == facade->GetCheckSum()); | ||||
|         const auto &input_bearings = route_parameters.bearings; | ||||
| 
 | ||||
|         std::vector<PhantomNode> phantom_node_list; | ||||
|         phantom_node_list.reserve(route_parameters.coordinates.size()); | ||||
| 
 | ||||
|         // find phantom nodes for all input coords
 | ||||
|         for (const auto i : util::irange<std::size_t>(0, route_parameters.coordinates.size())) | ||||
|         { | ||||
|             // if client hints are helpful, encode hints
 | ||||
|             if (checksum_OK && i < route_parameters.hints.size() && | ||||
|                 !route_parameters.hints[i].empty()) | ||||
|             { | ||||
|                 auto current_phantom_node = decodeBase64<PhantomNode>(route_parameters.hints[i]); | ||||
|                 if (current_phantom_node.IsValid(facade->GetNumberOfNodes())) | ||||
|                 { | ||||
|                     phantom_node_list.push_back(std::move(current_phantom_node)); | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
|             const int bearing = input_bearings.size() > 0 ? input_bearings[i].first : 0; | ||||
|             const int range = input_bearings.size() > 0 | ||||
|                                   ? (input_bearings[i].second ? *input_bearings[i].second : 10) | ||||
|                                   : 180; | ||||
|             auto results = | ||||
|                 facade->NearestPhantomNodes(route_parameters.coordinates[i], 1, bearing, range); | ||||
|             if (results.empty()) | ||||
|             { | ||||
|                 break; | ||||
|             } | ||||
|             phantom_node_list.push_back(std::move(results.front().phantom_node)); | ||||
|             BOOST_ASSERT(phantom_node_list.back().IsValid(facade->GetNumberOfNodes())); | ||||
|         } | ||||
| 
 | ||||
|         return phantom_node_list; | ||||
|     } | ||||
| 
 | ||||
|     // Object to hold all strongly connected components (scc) of a graph
 | ||||
|     // to access all graphs with component ID i, get the iterators by:
 | ||||
|     // auto start = std::begin(scc_component.component) + scc_component.range[i];
 | ||||
|     // auto end = std::begin(scc_component.component) + scc_component.range[i+1];
 | ||||
|     struct SCC_Component | ||||
|     { | ||||
|         // in_component: all NodeIDs sorted by component ID
 | ||||
|         // in_range: index where a new component starts
 | ||||
|         //
 | ||||
|         // example: NodeID 0, 1, 2, 4, 5 are in component 0
 | ||||
|         //          NodeID 3, 6, 7, 8    are in component 1
 | ||||
|         //          => in_component = [0, 1, 2, 4, 5, 3, 6, 7, 8]
 | ||||
|         //          => in_range = [0, 5]
 | ||||
|         SCC_Component(std::vector<NodeID> in_component_nodes, std::vector<size_t> in_range) | ||||
|             : component(std::move(in_component_nodes)), range(std::move(in_range)) | ||||
|         { | ||||
|             BOOST_ASSERT_MSG(component.size() > 0, "there's no scc component"); | ||||
|             BOOST_ASSERT_MSG(*std::max_element(range.begin(), range.end()) == component.size(), | ||||
|                              "scc component ranges are out of bound"); | ||||
|             BOOST_ASSERT_MSG(*std::min_element(range.begin(), range.end()) == 0, | ||||
|                              "invalid scc component range"); | ||||
|             BOOST_ASSERT_MSG(std::is_sorted(std::begin(range), std::end(range)), | ||||
|                              "invalid component ranges"); | ||||
|         } | ||||
| 
 | ||||
|         std::size_t GetNumberOfComponents() const | ||||
|         { | ||||
|             BOOST_ASSERT_MSG(range.size() > 0, "there's no range"); | ||||
|             return range.size() - 1; | ||||
|         } | ||||
| 
 | ||||
|         const std::vector<NodeID> component; | ||||
|         std::vector<std::size_t> range; | ||||
|     }; | ||||
| 
 | ||||
|     // takes the number of locations and its distance matrix,
 | ||||
|     // identifies and splits the graph in its strongly connected components (scc)
 | ||||
|     // and returns an SCC_Component
 | ||||
|     SCC_Component SplitUnaccessibleLocations(const std::size_t number_of_locations, | ||||
|                                              const util::DistTableWrapper<EdgeWeight> &result_table) | ||||
|     { | ||||
| 
 | ||||
|         if (std::find(std::begin(result_table), std::end(result_table), INVALID_EDGE_WEIGHT) == | ||||
|             std::end(result_table)) | ||||
|         { | ||||
|             // whole graph is one scc
 | ||||
|             std::vector<NodeID> location_ids(number_of_locations); | ||||
|             std::iota(std::begin(location_ids), std::end(location_ids), 0); | ||||
|             std::vector<size_t> range = {0, location_ids.size()}; | ||||
|             return SCC_Component(std::move(location_ids), std::move(range)); | ||||
|         } | ||||
| 
 | ||||
|         // Run TarjanSCC
 | ||||
|         auto wrapper = std::make_shared<util::MatrixGraphWrapper<EdgeWeight>>( | ||||
|             result_table.GetTable(), number_of_locations); | ||||
|         auto scc = extractor::TarjanSCC<util::MatrixGraphWrapper<EdgeWeight>>(wrapper); | ||||
|         scc.run(); | ||||
| 
 | ||||
|         const auto number_of_components = scc.get_number_of_components(); | ||||
| 
 | ||||
|         std::vector<std::size_t> range_insertion; | ||||
|         std::vector<std::size_t> range; | ||||
|         range_insertion.reserve(number_of_components); | ||||
|         range.reserve(number_of_components); | ||||
| 
 | ||||
|         std::vector<NodeID> components(number_of_locations, 0); | ||||
| 
 | ||||
|         std::size_t prefix = 0; | ||||
|         for (std::size_t j = 0; j < number_of_components; ++j) | ||||
|         { | ||||
|             range_insertion.push_back(prefix); | ||||
|             range.push_back(prefix); | ||||
|             prefix += scc.get_component_size(j); | ||||
|         } | ||||
|         // senitel
 | ||||
|         range.push_back(components.size()); | ||||
| 
 | ||||
|         for (std::size_t i = 0; i < number_of_locations; ++i) | ||||
|         { | ||||
|             components[range_insertion[scc.get_component_id(i)]] = i; | ||||
|             ++range_insertion[scc.get_component_id(i)]; | ||||
|         } | ||||
| 
 | ||||
|         return SCC_Component(std::move(components), std::move(range)); | ||||
|     } | ||||
| 
 | ||||
|     void SetLocPermutationOutput(const std::vector<NodeID> &permutation, | ||||
|                                  util::json::Object &json_result) | ||||
|     { | ||||
|         util::json::Array json_permutation; | ||||
|         json_permutation.values.insert(std::end(json_permutation.values), std::begin(permutation), | ||||
|                                        std::end(permutation)); | ||||
|         json_result.values["permutation"] = json_permutation; | ||||
|     } | ||||
| 
 | ||||
|     InternalRouteResult ComputeRoute(const std::vector<PhantomNode> &phantom_node_list, | ||||
|                                      const RouteParameters &route_parameters, | ||||
|                                      const std::vector<NodeID> &trip) | ||||
|                                      const api::TripParameters ¶meters, | ||||
|                                      const std::vector<NodeID> &trip); | ||||
| 
 | ||||
|   public: | ||||
|     explicit TripPlugin(datafacade::BaseDataFacade &facade_, const int max_locations_trip_) | ||||
|         : BasePlugin(facade_), shortest_path(&facade_, heaps), duration_table(&facade_, heaps), | ||||
|           max_locations_trip(max_locations_trip_) | ||||
|     { | ||||
|         InternalRouteResult min_route; | ||||
|         // given he final trip, compute total distance and return the route and location permutation
 | ||||
|         PhantomNodes viapoint; | ||||
|         const auto start = std::begin(trip); | ||||
|         const auto end = std::end(trip); | ||||
|         // computes a roundtrip from the nodes in trip
 | ||||
|         for (auto it = start; it != end; ++it) | ||||
|         { | ||||
|             const auto from_node = *it; | ||||
|             // if from_node is the last node, compute the route from the last to the first location
 | ||||
|             const auto to_node = std::next(it) != end ? *std::next(it) : *start; | ||||
| 
 | ||||
|             viapoint = PhantomNodes{phantom_node_list[from_node], phantom_node_list[to_node]}; | ||||
|             min_route.segment_end_coordinates.emplace_back(viapoint); | ||||
|         } | ||||
|         BOOST_ASSERT(min_route.segment_end_coordinates.size() == trip.size()); | ||||
| 
 | ||||
|         std::vector<bool> uturns(trip.size() + 1); | ||||
|         BOOST_ASSERT(route_parameters.uturns.size() > 0); | ||||
|         std::transform(trip.begin(), trip.end(), uturns.begin(), | ||||
|                        [&route_parameters](const NodeID idx) | ||||
|                        { | ||||
|                            return route_parameters.uturns[idx]; | ||||
|                        }); | ||||
|         BOOST_ASSERT(uturns.size() > 0); | ||||
|         uturns.back() = route_parameters.uturns[trip.front()]; | ||||
| 
 | ||||
|         search_engine_ptr->shortest_path(min_route.segment_end_coordinates, uturns, min_route); | ||||
| 
 | ||||
|         BOOST_ASSERT_MSG(min_route.shortest_path_length < INVALID_EDGE_WEIGHT, "unroutable route"); | ||||
|         return min_route; | ||||
|     } | ||||
| 
 | ||||
|     Status HandleRequest(const RouteParameters &route_parameters, | ||||
|                          util::json::Object &json_result) override final | ||||
|     { | ||||
|         if (max_locations_trip > 0 && | ||||
|             (static_cast<int>(route_parameters.coordinates.size()) > max_locations_trip)) | ||||
|         { | ||||
|             json_result.values["status_message"] = | ||||
|                 "Number of entries " + std::to_string(route_parameters.coordinates.size()) + | ||||
|                 " is higher than current maximum (" + std::to_string(max_locations_trip) + ")"; | ||||
|             return Status::Error; | ||||
|         } | ||||
| 
 | ||||
|         // check if all inputs are coordinates
 | ||||
|         if (!check_all_coordinates(route_parameters.coordinates)) | ||||
|         { | ||||
|             json_result.values["status_message"] = "Invalid coordinates"; | ||||
|             return Status::Error; | ||||
|         } | ||||
| 
 | ||||
|         const auto &input_bearings = route_parameters.bearings; | ||||
|         if (input_bearings.size() > 0 && | ||||
|             route_parameters.coordinates.size() != input_bearings.size()) | ||||
|         { | ||||
|             json_result.values["status_message"] = | ||||
|                 "Number of bearings does not match number of coordinates"; | ||||
|             return Status::Error; | ||||
|         } | ||||
| 
 | ||||
|         // get phantom nodes
 | ||||
|         auto phantom_node_list = GetPhantomNodes(route_parameters); | ||||
|         if (phantom_node_list.size() != route_parameters.coordinates.size()) | ||||
|         { | ||||
|             BOOST_ASSERT(phantom_node_list.size() < route_parameters.coordinates.size()); | ||||
|             json_result.values["status_message"] = | ||||
|                 std::string("Could not find a matching segment for coordinate ") + | ||||
|                 std::to_string(phantom_node_list.size()); | ||||
|             return Status::NoSegment; | ||||
|         } | ||||
| 
 | ||||
|         const auto number_of_locations = phantom_node_list.size(); | ||||
| 
 | ||||
|         // compute the distance table of all phantom nodes
 | ||||
|         const auto result_table = util::DistTableWrapper<EdgeWeight>( | ||||
|             *search_engine_ptr->distance_table(phantom_node_list, phantom_node_list), | ||||
|             number_of_locations); | ||||
| 
 | ||||
|         if (result_table.size() == 0) | ||||
|         { | ||||
|             return Status::Error; | ||||
|         } | ||||
| 
 | ||||
|         const constexpr std::size_t BF_MAX_FEASABLE = 10; | ||||
|         BOOST_ASSERT_MSG(result_table.size() == number_of_locations * number_of_locations, | ||||
|                          "Distance Table has wrong size"); | ||||
| 
 | ||||
|         // get scc components
 | ||||
|         SCC_Component scc = SplitUnaccessibleLocations(number_of_locations, result_table); | ||||
| 
 | ||||
|         using NodeIDIterator = typename std::vector<NodeID>::const_iterator; | ||||
| 
 | ||||
|         std::vector<std::vector<NodeID>> route_result; | ||||
|         route_result.reserve(scc.GetNumberOfComponents()); | ||||
|         // run Trip computation for every SCC
 | ||||
|         for (std::size_t k = 0; k < scc.GetNumberOfComponents(); ++k) | ||||
|         { | ||||
|             const auto component_size = scc.range[k + 1] - scc.range[k]; | ||||
| 
 | ||||
|             BOOST_ASSERT_MSG(component_size > 0, "invalid component size"); | ||||
| 
 | ||||
|             std::vector<NodeID> scc_route; | ||||
|             NodeIDIterator start = std::begin(scc.component) + scc.range[k]; | ||||
|             NodeIDIterator end = std::begin(scc.component) + scc.range[k + 1]; | ||||
| 
 | ||||
|             if (component_size > 1) | ||||
|             { | ||||
| 
 | ||||
|                 if (component_size < BF_MAX_FEASABLE) | ||||
|                 { | ||||
|                     scc_route = trip::BruteForceTrip(start, end, number_of_locations, result_table); | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     scc_route = | ||||
|                         trip::FarthestInsertionTrip(start, end, number_of_locations, result_table); | ||||
|                 } | ||||
| 
 | ||||
|                 // use this output if debugging of route is needed:
 | ||||
|                 // util::SimpleLogger().Write() << "Route #" << k << ": " << [&scc_route]()
 | ||||
|                 // {
 | ||||
|                 //     std::string s = "";
 | ||||
|                 //     for (auto x : scc_route)
 | ||||
|                 //     {
 | ||||
|                 //         s += std::to_string(x) + " ";
 | ||||
|                 //     }
 | ||||
|                 //     return s;
 | ||||
|                 // }();
 | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 scc_route = std::vector<NodeID>(start, end); | ||||
|             } | ||||
| 
 | ||||
|             route_result.push_back(std::move(scc_route)); | ||||
|         } | ||||
| 
 | ||||
|         // compute all round trip routes
 | ||||
|         std::vector<InternalRouteResult> comp_route; | ||||
|         comp_route.reserve(route_result.size()); | ||||
|         for (auto &elem : route_result) | ||||
|         { | ||||
|             comp_route.push_back(ComputeRoute(phantom_node_list, route_parameters, elem)); | ||||
|         } | ||||
| 
 | ||||
|         // prepare JSON output
 | ||||
|         // create a json object for every trip
 | ||||
|         util::json::Array trip; | ||||
|         for (std::size_t i = 0; i < route_result.size(); ++i) | ||||
|         { | ||||
|             util::json::Object scc_trip; | ||||
| 
 | ||||
|             // annotate comp_route[i] as a json trip
 | ||||
|             auto generator = MakeApiResponseGenerator(facade); | ||||
|             generator.DescribeRoute(route_parameters, comp_route[i], scc_trip); | ||||
| 
 | ||||
|             // set permutation output
 | ||||
|             SetLocPermutationOutput(route_result[i], scc_trip); | ||||
|             // set viaroute output
 | ||||
|             trip.values.push_back(std::move(scc_trip)); | ||||
|         } | ||||
| 
 | ||||
|         if (trip.values.empty()) | ||||
|         { | ||||
|             json_result.values["status_message"] = "Cannot find trips"; | ||||
|             return Status::EmptyResult; | ||||
|         } | ||||
| 
 | ||||
|         json_result.values["trips"] = std::move(trip); | ||||
|         json_result.values["status_message"] = "Found trips"; | ||||
|         return Status::Ok; | ||||
|     } | ||||
|     Status HandleRequest(const api::TripParameters ¶meters, util::json::Object &json_result); | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| #ifndef TRIP_BRUTE_FORCE_HPP | ||||
| #define TRIP_BRUTE_FORCE_HPP | ||||
| 
 | ||||
| #include "engine/search_engine.hpp" | ||||
| #include "util/typedefs.hpp" | ||||
| #include "util/dist_table_wrapper.hpp" | ||||
| #include "util/simple_logger.hpp" | ||||
| 
 | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| #ifndef TRIP_FARTHEST_INSERTION_HPP | ||||
| #define TRIP_FARTHEST_INSERTION_HPP | ||||
| 
 | ||||
| #include "engine/search_engine.hpp" | ||||
| #include "util/typedefs.hpp" | ||||
| #include "util/dist_table_wrapper.hpp" | ||||
| #include "util/typedefs.hpp" | ||||
| 
 | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| #ifndef TRIP_NEAREST_NEIGHBOUR_HPP | ||||
| #define TRIP_NEAREST_NEIGHBOUR_HPP | ||||
| 
 | ||||
| #include "engine/search_engine.hpp" | ||||
| #include "util/typedefs.hpp" | ||||
| #include "util/simple_logger.hpp" | ||||
| #include "util/dist_table_wrapper.hpp" | ||||
| 
 | ||||
|  | ||||
| @ -7,6 +7,7 @@ | ||||
| 
 | ||||
| #include <boost/spirit/include/qi_lit.hpp> | ||||
| #include <boost/spirit/include/qi_uint.hpp> | ||||
| #include <boost/spirit/include/qi_bool.hpp> | ||||
| #include <boost/spirit/include/qi_grammar.hpp> | ||||
| #include <boost/spirit/include/qi_action.hpp> | ||||
| #include <boost/spirit/include/qi_optional.hpp> | ||||
| @ -23,16 +24,53 @@ namespace qi = boost::spirit::qi; | ||||
| struct TripParametersGrammar final : public BaseParametersGrammar | ||||
| { | ||||
|     using Iterator = std::string::iterator; | ||||
|     using StepsT = bool; | ||||
|     using GeometriesT = engine::api::RouteParameters::GeometriesType; | ||||
|     using OverviewT = engine::api::RouteParameters::OverviewType; | ||||
| 
 | ||||
|     TripParametersGrammar() : BaseParametersGrammar(root_rule, parameters) | ||||
|     { | ||||
|         root_rule = "TODO(daniel-j-h)"; | ||||
|         const auto set_geojson_type = [this]() | ||||
|         { | ||||
|             parameters.geometries = engine::api::RouteParameters::GeometriesType::GeoJSON; | ||||
|         }; | ||||
|         const auto set_polyline_type = [this]() | ||||
|         { | ||||
|             parameters.geometries = engine::api::RouteParameters::GeometriesType::Polyline; | ||||
|         }; | ||||
| 
 | ||||
|         const auto set_simplified_type = [this]() | ||||
|         { | ||||
|             parameters.overview = engine::api::RouteParameters::OverviewType::Simplified; | ||||
|         }; | ||||
|         const auto set_full_type = [this]() | ||||
|         { | ||||
|             parameters.overview = engine::api::RouteParameters::OverviewType::Full; | ||||
|         }; | ||||
|         const auto set_false_type = [this]() | ||||
|         { | ||||
|             parameters.overview = engine::api::RouteParameters::OverviewType::False; | ||||
|         }; | ||||
|         const auto set_steps = [this](const StepsT steps) | ||||
|         { | ||||
|             parameters.steps = steps; | ||||
|         }; | ||||
| 
 | ||||
|         steps_rule = qi::lit("steps=") >> qi::bool_; | ||||
|         geometries_rule = qi::lit("geometries=geojson")[set_geojson_type] | | ||||
|                           qi::lit("geometries=polyline")[set_polyline_type]; | ||||
|         overview_rule = qi::lit("overview=simplified")[set_simplified_type] | | ||||
|                         qi::lit("overview=full")[set_full_type] | | ||||
|                         qi::lit("overview=false")[set_false_type]; | ||||
|         trip_rule = steps_rule[set_steps] | geometries_rule | overview_rule; | ||||
|         root_rule = -((base_rule | trip_rule) % '&'); | ||||
|     } | ||||
| 
 | ||||
|     engine::api::TripParameters parameters; | ||||
| 
 | ||||
|   private: | ||||
|     qi::rule<Iterator> root_rule, trip_rule; | ||||
|     qi::rule<Iterator> root_rule, trip_rule, geometries_rule, overview_rule; | ||||
|     qi::rule<Iterator, StepsT()> steps_rule; | ||||
| }; | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -7,7 +7,7 @@ | ||||
| //#include "engine/plugins/hello_world.hpp"
 | ||||
| #include "engine/plugins/nearest.hpp" | ||||
| //#include "engine/plugins/timestamp.hpp"
 | ||||
| //#include "engine/plugins/trip.hpp"
 | ||||
| #include "engine/plugins/trip.hpp" | ||||
| #include "engine/plugins/viaroute.hpp" | ||||
| //#include "engine/plugins/tile.hpp"
 | ||||
| #include "engine/plugins/match.hpp" | ||||
| @ -146,7 +146,7 @@ Engine::Engine(EngineConfig &config) | ||||
|     route_plugin = create<ViaRoutePlugin>(*query_data_facade, config.max_locations_viaroute); | ||||
|     table_plugin = create<TablePlugin>(*query_data_facade, config.max_locations_distance_table); | ||||
|     nearest_plugin = create<NearestPlugin>(*query_data_facade); | ||||
|     // trip_plugin = ceate<TripPlugin>(*query_data_facade, config.max_locations_trip);
 | ||||
|     trip_plugin = create<TripPlugin>(*query_data_facade, config.max_locations_trip); | ||||
|     match_plugin = create<MatchPlugin>(*query_data_facade, config.max_locations_map_matching); | ||||
| } | ||||
| 
 | ||||
| @ -172,8 +172,7 @@ Status Engine::Nearest(const api::NearestParameters ¶ms, util::json::Object | ||||
| 
 | ||||
| Status Engine::Trip(const api::TripParameters ¶ms, util::json::Object &result) | ||||
| { | ||||
|     // return RunQuery(lock, *query_data_facade, params, *trip_plugin, result);
 | ||||
|     return Status::Error; | ||||
|     return RunQuery(lock, *query_data_facade, params, *trip_plugin, result); | ||||
| } | ||||
| 
 | ||||
| Status Engine::Match(const api::MatchParameters ¶ms, util::json::Object &result) | ||||
|  | ||||
| @ -163,7 +163,7 @@ Status MatchPlugin::HandleRequest(const api::MatchParameters ¶meters, | ||||
|     std::vector<InternalRouteResult> sub_routes(sub_matchings.size()); | ||||
|     for (auto index : util::irange(0UL, sub_matchings.size())) | ||||
|     { | ||||
|         BOOST_ASSERT(sub.nodes.size() > 1); | ||||
|         BOOST_ASSERT(sub_matchings[index].nodes.size() > 1); | ||||
| 
 | ||||
|         // FIXME we only run this to obtain the geometry
 | ||||
|         // The clean way would be to get this directly from the map matching plugin
 | ||||
| @ -177,7 +177,7 @@ Status MatchPlugin::HandleRequest(const api::MatchParameters ¶meters, | ||||
|             sub_routes[index].segment_end_coordinates.emplace_back(current_phantom_node_pair); | ||||
|         } | ||||
|         shortest_path(sub_routes[index].segment_end_coordinates, {}, sub_routes[index]); | ||||
|         BOOST_ASSERT(raw_route.shortest_path_length != INVALID_EDGE_WEIGHT); | ||||
|         BOOST_ASSERT(sub_routes[index].shortest_path_length != INVALID_EDGE_WEIGHT); | ||||
|     } | ||||
| 
 | ||||
|     api::MatchAPI match_api{BasePlugin::facade, parameters}; | ||||
|  | ||||
							
								
								
									
										258
									
								
								src/engine/plugins/trip.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								src/engine/plugins/trip.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,258 @@ | ||||
| #include "engine/plugins/trip.hpp" | ||||
| 
 | ||||
| #include "extractor/tarjan_scc.hpp" | ||||
| 
 | ||||
| #include "engine/api/trip_api.hpp" | ||||
| #include "engine/api/trip_parameters.hpp" | ||||
| #include "engine/trip/trip_nearest_neighbour.hpp" | ||||
| #include "engine/trip/trip_farthest_insertion.hpp" | ||||
| #include "engine/trip/trip_brute_force.hpp" | ||||
| #include "util/dist_table_wrapper.hpp"   // to access the dist table more easily
 | ||||
| #include "util/matrix_graph_wrapper.hpp" // wrapper to use tarjan scc on dist table
 | ||||
| #include "util/json_container.hpp" | ||||
| 
 | ||||
| #include <boost/assert.hpp> | ||||
| 
 | ||||
| #include <cstdlib> | ||||
| #include <algorithm> | ||||
| #include <memory> | ||||
| #include <string> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
| #include <iterator> | ||||
| 
 | ||||
| namespace osrm | ||||
| { | ||||
| namespace engine | ||||
| { | ||||
| namespace plugins | ||||
| { | ||||
| 
 | ||||
| // Object to hold all strongly connected components (scc) of a graph
 | ||||
| // to access all graphs with component ID i, get the iterators by:
 | ||||
| // auto start = std::begin(scc_component.component) + scc_component.range[i];
 | ||||
| // auto end = std::begin(scc_component.component) + scc_component.range[i+1];
 | ||||
| struct SCC_Component | ||||
| { | ||||
|     // in_component: all NodeIDs sorted by component ID
 | ||||
|     // in_range: index where a new component starts
 | ||||
|     //
 | ||||
|     // example: NodeID 0, 1, 2, 4, 5 are in component 0
 | ||||
|     //          NodeID 3, 6, 7, 8    are in component 1
 | ||||
|     //          => in_component = [0, 1, 2, 4, 5, 3, 6, 7, 8]
 | ||||
|     //          => in_range = [0, 5]
 | ||||
|     SCC_Component(std::vector<NodeID> in_component_nodes, std::vector<size_t> in_range) | ||||
|         : component(std::move(in_component_nodes)), range(std::move(in_range)) | ||||
|     { | ||||
|         BOOST_ASSERT_MSG(component.size() > 0, "there's no scc component"); | ||||
|         BOOST_ASSERT_MSG(*std::max_element(range.begin(), range.end()) == component.size(), | ||||
|                          "scc component ranges are out of bound"); | ||||
|         BOOST_ASSERT_MSG(*std::min_element(range.begin(), range.end()) == 0, | ||||
|                          "invalid scc component range"); | ||||
|         BOOST_ASSERT_MSG(std::is_sorted(std::begin(range), std::end(range)), | ||||
|                          "invalid component ranges"); | ||||
|     } | ||||
| 
 | ||||
|     std::size_t GetNumberOfComponents() const | ||||
|     { | ||||
|         BOOST_ASSERT_MSG(range.size() > 0, "there's no range"); | ||||
|         return range.size() - 1; | ||||
|     } | ||||
| 
 | ||||
|     const std::vector<NodeID> component; | ||||
|     std::vector<std::size_t> range; | ||||
| }; | ||||
| 
 | ||||
| // takes the number of locations and its duration matrix,
 | ||||
| // identifies and splits the graph in its strongly connected components (scc)
 | ||||
| // and returns an SCC_Component
 | ||||
| SCC_Component SplitUnaccessibleLocations(const std::size_t number_of_locations, | ||||
|                                          const util::DistTableWrapper<EdgeWeight> &result_table) | ||||
| { | ||||
| 
 | ||||
|     if (std::find(std::begin(result_table), std::end(result_table), INVALID_EDGE_WEIGHT) == | ||||
|         std::end(result_table)) | ||||
|     { | ||||
|         // whole graph is one scc
 | ||||
|         std::vector<NodeID> location_ids(number_of_locations); | ||||
|         std::iota(std::begin(location_ids), std::end(location_ids), 0); | ||||
|         std::vector<size_t> range = {0, location_ids.size()}; | ||||
|         return SCC_Component(std::move(location_ids), std::move(range)); | ||||
|     } | ||||
| 
 | ||||
|     // Run TarjanSCC
 | ||||
|     auto wrapper = std::make_shared<util::MatrixGraphWrapper<EdgeWeight>>(result_table.GetTable(), | ||||
|                                                                           number_of_locations); | ||||
|     auto scc = extractor::TarjanSCC<util::MatrixGraphWrapper<EdgeWeight>>(wrapper); | ||||
|     scc.run(); | ||||
| 
 | ||||
|     const auto number_of_components = scc.get_number_of_components(); | ||||
| 
 | ||||
|     std::vector<std::size_t> range_insertion; | ||||
|     std::vector<std::size_t> range; | ||||
|     range_insertion.reserve(number_of_components); | ||||
|     range.reserve(number_of_components); | ||||
| 
 | ||||
|     std::vector<NodeID> components(number_of_locations, 0); | ||||
| 
 | ||||
|     std::size_t prefix = 0; | ||||
|     for (std::size_t j = 0; j < number_of_components; ++j) | ||||
|     { | ||||
|         range_insertion.push_back(prefix); | ||||
|         range.push_back(prefix); | ||||
|         prefix += scc.get_component_size(j); | ||||
|     } | ||||
|     // senitel
 | ||||
|     range.push_back(components.size()); | ||||
| 
 | ||||
|     for (std::size_t i = 0; i < number_of_locations; ++i) | ||||
|     { | ||||
|         components[range_insertion[scc.get_component_id(i)]] = i; | ||||
|         ++range_insertion[scc.get_component_id(i)]; | ||||
|     } | ||||
| 
 | ||||
|     return SCC_Component(std::move(components), std::move(range)); | ||||
| } | ||||
| 
 | ||||
| InternalRouteResult TripPlugin::ComputeRoute(const std::vector<PhantomNode> &snapped_phantoms, | ||||
|                                              const api::TripParameters ¶meters, | ||||
|                                              const std::vector<NodeID> &trip) | ||||
| { | ||||
|     InternalRouteResult min_route; | ||||
|     // given he final trip, compute total duration and return the route and location permutation
 | ||||
|     PhantomNodes viapoint; | ||||
|     const auto start = std::begin(trip); | ||||
|     const auto end = std::end(trip); | ||||
|     // computes a roundtrip from the nodes in trip
 | ||||
|     for (auto it = start; it != end; ++it) | ||||
|     { | ||||
|         const auto from_node = *it; | ||||
|         // if from_node is the last node, compute the route from the last to the first location
 | ||||
|         const auto to_node = std::next(it) != end ? *std::next(it) : *start; | ||||
| 
 | ||||
|         viapoint = PhantomNodes{snapped_phantoms[from_node], snapped_phantoms[to_node]}; | ||||
|         min_route.segment_end_coordinates.emplace_back(viapoint); | ||||
|     } | ||||
|     BOOST_ASSERT(min_route.segment_end_coordinates.size() == trip.size()); | ||||
| 
 | ||||
|     std::vector<boost::optional<bool>> uturns; | ||||
|     if (parameters.uturns.size() > 0) | ||||
|     { | ||||
|         uturns.resize(trip.size() + 1); | ||||
|         std::transform(trip.begin(), trip.end(), uturns.begin(), [¶meters](const NodeID idx) | ||||
|                 { | ||||
|                 return parameters.uturns[idx]; | ||||
|                 }); | ||||
|         BOOST_ASSERT(uturns.size() > 0); | ||||
|         uturns.back() = parameters.uturns[trip.front()]; | ||||
|     } | ||||
| 
 | ||||
|     shortest_path(min_route.segment_end_coordinates, uturns, min_route); | ||||
| 
 | ||||
|     BOOST_ASSERT_MSG(min_route.shortest_path_length < INVALID_EDGE_WEIGHT, "unroutable route"); | ||||
|     return min_route; | ||||
| } | ||||
| 
 | ||||
| Status TripPlugin::HandleRequest(const api::TripParameters ¶meters, | ||||
|                                  util::json::Object &json_result) | ||||
| { | ||||
|     BOOST_ASSERT(parameters.IsValid()); | ||||
| 
 | ||||
|     // enforce maximum number of locations for performance reasons
 | ||||
|     if (max_locations_trip > 0 && | ||||
|         static_cast<int>(parameters.coordinates.size()) > max_locations_trip) | ||||
|     { | ||||
|         return Error("TooBig", "Too many trip coordinates", json_result); | ||||
|     } | ||||
| 
 | ||||
|     if (!CheckAllCoordinates(parameters.coordinates)) | ||||
|     { | ||||
|         return Error("InvalidValue", "Invalid coordinate value.", json_result); | ||||
|     } | ||||
| 
 | ||||
|     auto phantom_node_pairs = GetPhantomNodes(parameters); | ||||
|     if (phantom_node_pairs.size() != parameters.coordinates.size()) | ||||
|     { | ||||
|         return Error("no-segment", | ||||
|                      std::string("Could not find a matching segment for coordinate ") + | ||||
|                          std::to_string(phantom_node_pairs.size()), | ||||
|                      json_result); | ||||
|     } | ||||
|     BOOST_ASSERT(phantom_node_pairs.size() == parameters.coordinates.size()); | ||||
| 
 | ||||
|     auto snapped_phantoms = SnapPhantomNodes(phantom_node_pairs); | ||||
| 
 | ||||
|     const auto number_of_locations = snapped_phantoms.size(); | ||||
| 
 | ||||
|     // compute the duration table of all phantom nodes
 | ||||
|     const auto result_table = util::DistTableWrapper<EdgeWeight>( | ||||
|         duration_table(snapped_phantoms), number_of_locations); | ||||
| 
 | ||||
|     if (result_table.size() == 0) | ||||
|     { | ||||
|         return Status::Error; | ||||
|     } | ||||
| 
 | ||||
|     const constexpr std::size_t BF_MAX_FEASABLE = 10; | ||||
|     BOOST_ASSERT_MSG(result_table.size() == number_of_locations * number_of_locations, | ||||
|                      "Distance Table has wrong size"); | ||||
| 
 | ||||
|     // get scc components
 | ||||
|     SCC_Component scc = SplitUnaccessibleLocations(number_of_locations, result_table); | ||||
| 
 | ||||
|     using NodeIDIterator = typename std::vector<NodeID>::const_iterator; | ||||
| 
 | ||||
|     std::vector<std::vector<NodeID>> trips; | ||||
|     trips.reserve(scc.GetNumberOfComponents()); | ||||
|     // run Trip computation for every SCC
 | ||||
|     for (std::size_t k = 0; k < scc.GetNumberOfComponents(); ++k) | ||||
|     { | ||||
|         const auto component_size = scc.range[k + 1] - scc.range[k]; | ||||
| 
 | ||||
|         BOOST_ASSERT_MSG(component_size > 0, "invalid component size"); | ||||
| 
 | ||||
|         std::vector<NodeID> scc_route; | ||||
|         NodeIDIterator start = std::begin(scc.component) + scc.range[k]; | ||||
|         NodeIDIterator end = std::begin(scc.component) + scc.range[k + 1]; | ||||
| 
 | ||||
|         if (component_size > 1) | ||||
|         { | ||||
| 
 | ||||
|             if (component_size < BF_MAX_FEASABLE) | ||||
|             { | ||||
|                 scc_route = trip::BruteForceTrip(start, end, number_of_locations, result_table); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 scc_route = | ||||
|                     trip::FarthestInsertionTrip(start, end, number_of_locations, result_table); | ||||
|             } | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             scc_route = std::vector<NodeID>(start, end); | ||||
|         } | ||||
| 
 | ||||
|         trips.push_back(std::move(scc_route)); | ||||
|     } | ||||
|     if (trips.empty()) | ||||
|     { | ||||
|         return Error("NoTrips", "Cannot find trips", json_result); | ||||
|     } | ||||
| 
 | ||||
|     // compute all round trip routes
 | ||||
|     std::vector<InternalRouteResult> routes; | ||||
|     routes.reserve(trips.size()); | ||||
|     for (auto &trip : trips) | ||||
|     { | ||||
|         routes.push_back(ComputeRoute(snapped_phantoms, parameters, trip)); | ||||
|     } | ||||
| 
 | ||||
|     api::TripAPI trip_api{BasePlugin::facade, parameters}; | ||||
|     trip_api.MakeResponse(trips, routes, snapped_phantoms, json_result); | ||||
| 
 | ||||
|     return Status::Ok; | ||||
| } | ||||
| } | ||||
| } | ||||
| } | ||||
| @ -1,4 +1,5 @@ | ||||
| #include "server/service/trip_service.hpp" | ||||
| #include "server/service/utils.hpp" | ||||
| 
 | ||||
| #include "engine/api/trip_parameters.hpp" | ||||
| #include "server/api/parameters_parser.hpp" | ||||
| @ -13,14 +14,60 @@ namespace server | ||||
| { | ||||
| namespace service | ||||
| { | ||||
| namespace | ||||
| { | ||||
| std::string getWrongOptionHelp(const engine::api::TripParameters ¶meters) | ||||
| { | ||||
|     std::string help; | ||||
| 
 | ||||
|     const auto coord_size = parameters.coordinates.size(); | ||||
| 
 | ||||
|     const bool param_size_mismatch = constrainParamSize(PARAMETER_SIZE_MISMATCH_MSG, "hints", | ||||
|                                                         parameters.hints, coord_size, help) || | ||||
|                                      constrainParamSize(PARAMETER_SIZE_MISMATCH_MSG, "bearings", | ||||
|                                                         parameters.bearings, coord_size, help) || | ||||
|                                      constrainParamSize(PARAMETER_SIZE_MISMATCH_MSG, "radiuses", | ||||
|                                                         parameters.radiuses, coord_size, help); | ||||
| 
 | ||||
|     if (!param_size_mismatch && parameters.coordinates.size() < 2) | ||||
|     { | ||||
|         help = "Number of coordinates needs to be at least two."; | ||||
|     } | ||||
| 
 | ||||
|     return help; | ||||
| } | ||||
| } // anon. ns
 | ||||
| 
 | ||||
| engine::Status TripService::RunQuery(std::vector<util::FixedPointCoordinate> coordinates, | ||||
|                                      std::string &options, | ||||
|                                      util::json::Object &result) | ||||
|                                       std::string &options, | ||||
|                                       util::json::Object &result) | ||||
| { | ||||
|     // TODO(daniel-j-h)
 | ||||
|     return Status::Error; | ||||
| } | ||||
|     auto options_iterator = options.begin(); | ||||
|     auto parameters = | ||||
|         api::parseParameters<engine::api::TripParameters>(options_iterator, options.end()); | ||||
|     if (!parameters || options_iterator != options.end()) | ||||
|     { | ||||
|         const auto position = std::distance(options.begin(), options_iterator); | ||||
|         result.values["code"] = "invalid-options"; | ||||
|         result.values["message"] = | ||||
|             "Options string malformed close to position " + std::to_string(position); | ||||
|         return engine::Status::Error; | ||||
|     } | ||||
| 
 | ||||
|     BOOST_ASSERT(parameters); | ||||
|     parameters->coordinates = std::move(coordinates); | ||||
| 
 | ||||
|     if (!parameters->IsValid()) | ||||
|     { | ||||
|         result.values["code"] = "invalid-options"; | ||||
|         result.values["message"] = getWrongOptionHelp(*parameters); | ||||
|         return engine::Status::Error; | ||||
|     } | ||||
|     BOOST_ASSERT(parameters->IsValid()); | ||||
| 
 | ||||
|     return BaseService::routing_machine.Trip(*parameters, result); | ||||
| } | ||||
| 
 | ||||
| } | ||||
| } | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user