Initial non-building match plugin
This commit is contained in:
parent
aa79c41804
commit
487df70eb3
@ -14,8 +14,11 @@ namespace api
|
|||||||
|
|
||||||
struct MatchParameters : public RouteParameters
|
struct MatchParameters : public RouteParameters
|
||||||
{
|
{
|
||||||
std::vector<unsigned> timestamps;
|
std::vector<boost::optional<unsigned>> timestamps;
|
||||||
bool IsValid() const;
|
bool IsValid() const
|
||||||
|
{
|
||||||
|
return timestamps.empty() || timestamps.size() == coordinates.size();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,25 +2,12 @@
|
|||||||
#define MATCH_HPP
|
#define MATCH_HPP
|
||||||
|
|
||||||
#include "engine/plugins/plugin_base.hpp"
|
#include "engine/plugins/plugin_base.hpp"
|
||||||
|
#include "engine/api/match_parameters.hpp"
|
||||||
|
|
||||||
#include "engine/map_matching/bayes_classifier.hpp"
|
#include "engine/map_matching/bayes_classifier.hpp"
|
||||||
#include "engine/object_encoder.hpp"
|
|
||||||
#include "engine/search_engine.hpp"
|
|
||||||
#include "engine/guidance/textual_route_annotation.hpp"
|
|
||||||
#include "engine/guidance/segment_list.hpp"
|
|
||||||
#include "engine/api_response_generator.hpp"
|
|
||||||
#include "engine/routing_algorithms/map_matching.hpp"
|
#include "engine/routing_algorithms/map_matching.hpp"
|
||||||
#include "util/coordinate_calculation.hpp"
|
|
||||||
#include "util/integer_range.hpp"
|
|
||||||
#include "util/json_logger.hpp"
|
|
||||||
#include "util/json_util.hpp"
|
#include "util/json_util.hpp"
|
||||||
#include "util/string_util.hpp"
|
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace osrm
|
namespace osrm
|
||||||
@ -30,10 +17,8 @@ namespace engine
|
|||||||
namespace plugins
|
namespace plugins
|
||||||
{
|
{
|
||||||
|
|
||||||
template <class DataFacadeT> class MapMatchingPlugin : public BasePlugin
|
class MatchingPlugin : public BasePlugin
|
||||||
{
|
{
|
||||||
std::shared_ptr<SearchEngine<DataFacadeT>> search_engine_ptr;
|
|
||||||
|
|
||||||
using SubMatching = routing_algorithms::SubMatching;
|
using SubMatching = routing_algorithms::SubMatching;
|
||||||
using SubMatchingList = routing_algorithms::SubMatchingList;
|
using SubMatchingList = routing_algorithms::SubMatchingList;
|
||||||
using CandidateLists = routing_algorithms::CandidateLists;
|
using CandidateLists = routing_algorithms::CandidateLists;
|
||||||
@ -43,8 +28,8 @@ template <class DataFacadeT> class MapMatchingPlugin : public BasePlugin
|
|||||||
using TraceClassification = ClassifierT::ClassificationT;
|
using TraceClassification = ClassifierT::ClassificationT;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
MapMatchingPlugin(DataFacadeT *facade, const int max_locations_map_matching)
|
MatchingPlugin(datafacade::BaseDataFacade &facade_, const int max_locations_map_matching)
|
||||||
: descriptor_string("match"), facade(facade),
|
: BasePlugin(facade_),
|
||||||
max_locations_map_matching(max_locations_map_matching),
|
max_locations_map_matching(max_locations_map_matching),
|
||||||
// the values were derived from fitting a laplace distribution
|
// the values were derived from fitting a laplace distribution
|
||||||
// to the values of manually classified traces
|
// to the values of manually classified traces
|
||||||
@ -52,332 +37,11 @@ template <class DataFacadeT> class MapMatchingPlugin : public BasePlugin
|
|||||||
map_matching::LaplaceDistribution(0.054385, 0.458432),
|
map_matching::LaplaceDistribution(0.054385, 0.458432),
|
||||||
0.696774) // valid apriori probability
|
0.696774) // valid apriori probability
|
||||||
{
|
{
|
||||||
search_engine_ptr = std::make_shared<SearchEngine<DataFacadeT>>(facade);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual ~MapMatchingPlugin() {}
|
Status HandleRequest(const MatchParameters ¶meters, util::json::Object &json_result);
|
||||||
|
|
||||||
const std::string GetDescriptor() const final override { return descriptor_string; }
|
|
||||||
|
|
||||||
TraceClassification
|
|
||||||
classify(const float trace_length, const float matched_length, const int removed_points) const
|
|
||||||
{
|
|
||||||
(void)removed_points; // unused
|
|
||||||
|
|
||||||
const double distance_feature = -std::log(trace_length) + std::log(matched_length);
|
|
||||||
|
|
||||||
// matched to the same point
|
|
||||||
if (!std::isfinite(distance_feature))
|
|
||||||
{
|
|
||||||
return std::make_pair(ClassifierT::ClassLabel::NEGATIVE, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto label_with_confidence = classifier.classify(distance_feature);
|
|
||||||
|
|
||||||
return label_with_confidence;
|
|
||||||
}
|
|
||||||
|
|
||||||
CandidateLists getCandidates(
|
|
||||||
const std::vector<util::FixedPointCoordinate> &input_coords,
|
|
||||||
const std::vector<std::pair<const int, const boost::optional<int>>> &input_bearings,
|
|
||||||
const double gps_precision,
|
|
||||||
std::vector<double> &sub_trace_lengths)
|
|
||||||
{
|
|
||||||
CandidateLists candidates_lists;
|
|
||||||
|
|
||||||
// assuming gps_precision is the standard deviation of a normal distribution that
|
|
||||||
// models GPS noise (in this model), this should give us the correct search radius
|
|
||||||
// with > 99% confidence
|
|
||||||
double query_radius = 3 * gps_precision;
|
|
||||||
double last_distance =
|
|
||||||
util::coordinate_calculation::haversineDistance(input_coords[0], input_coords[1]);
|
|
||||||
|
|
||||||
sub_trace_lengths.resize(input_coords.size());
|
|
||||||
sub_trace_lengths[0] = 0;
|
|
||||||
for (const auto current_coordinate : util::irange<std::size_t>(0, input_coords.size()))
|
|
||||||
{
|
|
||||||
bool allow_uturn = false;
|
|
||||||
if (0 < current_coordinate)
|
|
||||||
{
|
|
||||||
last_distance = util::coordinate_calculation::haversineDistance(
|
|
||||||
input_coords[current_coordinate - 1], input_coords[current_coordinate]);
|
|
||||||
|
|
||||||
sub_trace_lengths[current_coordinate] +=
|
|
||||||
sub_trace_lengths[current_coordinate - 1] + last_distance;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input_coords.size() - 1 > current_coordinate && 0 < current_coordinate)
|
|
||||||
{
|
|
||||||
double turn_angle = util::coordinate_calculation::computeAngle(
|
|
||||||
input_coords[current_coordinate - 1], input_coords[current_coordinate],
|
|
||||||
input_coords[current_coordinate + 1]);
|
|
||||||
|
|
||||||
// sharp turns indicate a possible uturn
|
|
||||||
if (turn_angle <= 90.0 || turn_angle >= 270.0)
|
|
||||||
{
|
|
||||||
allow_uturn = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use bearing values if supplied, otherwise fallback to 0,180 defaults
|
|
||||||
auto bearing = input_bearings.size() > 0 ? input_bearings[current_coordinate].first : 0;
|
|
||||||
auto range = input_bearings.size() > 0
|
|
||||||
? (input_bearings[current_coordinate].second
|
|
||||||
? *input_bearings[current_coordinate].second
|
|
||||||
: 10)
|
|
||||||
: 180;
|
|
||||||
auto candidates = facade->NearestPhantomNodesInRange(input_coords[current_coordinate],
|
|
||||||
query_radius, bearing, range);
|
|
||||||
|
|
||||||
if (candidates.size() == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort by forward id, then by reverse id and then by distance
|
|
||||||
std::sort(
|
|
||||||
candidates.begin(), candidates.end(),
|
|
||||||
[](const PhantomNodeWithDistance &lhs, const PhantomNodeWithDistance &rhs)
|
|
||||||
{
|
|
||||||
return lhs.phantom_node.forward_node_id < rhs.phantom_node.forward_node_id ||
|
|
||||||
(lhs.phantom_node.forward_node_id == rhs.phantom_node.forward_node_id &&
|
|
||||||
(lhs.phantom_node.reverse_node_id < rhs.phantom_node.reverse_node_id ||
|
|
||||||
(lhs.phantom_node.reverse_node_id ==
|
|
||||||
rhs.phantom_node.reverse_node_id &&
|
|
||||||
lhs.distance < rhs.distance)));
|
|
||||||
});
|
|
||||||
|
|
||||||
auto new_end = std::unique(
|
|
||||||
candidates.begin(), candidates.end(),
|
|
||||||
[](const PhantomNodeWithDistance &lhs, const PhantomNodeWithDistance &rhs)
|
|
||||||
{
|
|
||||||
return lhs.phantom_node.forward_node_id == rhs.phantom_node.forward_node_id &&
|
|
||||||
lhs.phantom_node.reverse_node_id == rhs.phantom_node.reverse_node_id;
|
|
||||||
});
|
|
||||||
candidates.resize(new_end - candidates.begin());
|
|
||||||
|
|
||||||
if (!allow_uturn)
|
|
||||||
{
|
|
||||||
const auto compact_size = candidates.size();
|
|
||||||
for (const auto i : util::irange<std::size_t>(0, compact_size))
|
|
||||||
{
|
|
||||||
// Split edge if it is bidirectional and append reverse direction to end of list
|
|
||||||
if (candidates[i].phantom_node.forward_node_id != SPECIAL_NODEID &&
|
|
||||||
candidates[i].phantom_node.reverse_node_id != SPECIAL_NODEID)
|
|
||||||
{
|
|
||||||
PhantomNode reverse_node(candidates[i].phantom_node);
|
|
||||||
reverse_node.forward_node_id = SPECIAL_NODEID;
|
|
||||||
candidates.push_back(
|
|
||||||
PhantomNodeWithDistance{reverse_node, candidates[i].distance});
|
|
||||||
|
|
||||||
candidates[i].phantom_node.reverse_node_id = SPECIAL_NODEID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort by distance to make pruning effective
|
|
||||||
std::sort(candidates.begin(), candidates.end(),
|
|
||||||
[](const PhantomNodeWithDistance &lhs, const PhantomNodeWithDistance &rhs)
|
|
||||||
{
|
|
||||||
return lhs.distance < rhs.distance;
|
|
||||||
});
|
|
||||||
|
|
||||||
candidates_lists.push_back(std::move(candidates));
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidates_lists;
|
|
||||||
}
|
|
||||||
|
|
||||||
util::json::Object submatchingToJSON(const SubMatching &sub,
|
|
||||||
const RouteParameters &route_parameters,
|
|
||||||
const InternalRouteResult &raw_route)
|
|
||||||
{
|
|
||||||
util::json::Object subtrace;
|
|
||||||
|
|
||||||
if (route_parameters.classify)
|
|
||||||
{
|
|
||||||
subtrace.values["confidence"] = sub.confidence;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto response_generator = MakeApiResponseGenerator(facade);
|
|
||||||
|
|
||||||
subtrace.values["hint_data"] = response_generator.BuildHintData(raw_route);
|
|
||||||
|
|
||||||
if (route_parameters.geometry || route_parameters.print_instructions)
|
|
||||||
{
|
|
||||||
using SegmentList = guidance::SegmentList<DataFacadeT>;
|
|
||||||
// Passing false to extract_alternative extracts the route.
|
|
||||||
const constexpr bool EXTRACT_ROUTE = false;
|
|
||||||
// by passing false to segment_list, we skip the douglas peucker simplification
|
|
||||||
// and mark all segments as necessary within the generation process
|
|
||||||
const constexpr bool NO_ROUTE_SIMPLIFICATION = false;
|
|
||||||
SegmentList segment_list(raw_route, EXTRACT_ROUTE, route_parameters.zoom_level,
|
|
||||||
NO_ROUTE_SIMPLIFICATION, facade);
|
|
||||||
|
|
||||||
if (route_parameters.geometry)
|
|
||||||
{
|
|
||||||
subtrace.values["geometry"] =
|
|
||||||
response_generator.GetGeometry(route_parameters.compression, segment_list);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (route_parameters.print_instructions)
|
|
||||||
{
|
|
||||||
subtrace.values["instructions"] =
|
|
||||||
guidance::AnnotateRoute<DataFacadeT>(segment_list.Get(), facade);
|
|
||||||
}
|
|
||||||
|
|
||||||
util::json::Object json_route_summary;
|
|
||||||
json_route_summary.values["total_distance"] = segment_list.GetDistance();
|
|
||||||
json_route_summary.values["total_time"] = segment_list.GetDuration();
|
|
||||||
subtrace.values["route_summary"] = json_route_summary;
|
|
||||||
}
|
|
||||||
|
|
||||||
subtrace.values["indices"] = util::json::make_array(sub.indices);
|
|
||||||
|
|
||||||
util::json::Array points;
|
|
||||||
for (const auto &node : sub.nodes)
|
|
||||||
{
|
|
||||||
points.values.emplace_back(
|
|
||||||
util::json::make_array(node.location.lat / COORDINATE_PRECISION,
|
|
||||||
node.location.lon / COORDINATE_PRECISION));
|
|
||||||
}
|
|
||||||
subtrace.values["matched_points"] = points;
|
|
||||||
|
|
||||||
util::json::Array names;
|
|
||||||
for (const auto &node : sub.nodes)
|
|
||||||
{
|
|
||||||
names.values.emplace_back(facade->get_name_for_id(node.name_id));
|
|
||||||
}
|
|
||||||
subtrace.values["matched_names"] = names;
|
|
||||||
|
|
||||||
return subtrace;
|
|
||||||
}
|
|
||||||
|
|
||||||
Status HandleRequest(const RouteParameters &route_parameters,
|
|
||||||
util::json::Object &json_result) final override
|
|
||||||
{
|
|
||||||
// enforce maximum number of locations for performance reasons
|
|
||||||
if (max_locations_map_matching > 0 &&
|
|
||||||
static_cast<int>(route_parameters.coordinates.size()) > max_locations_map_matching)
|
|
||||||
{
|
|
||||||
json_result.values["status_message"] = "Too many coordinates";
|
|
||||||
return Status::Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check number of parameters
|
|
||||||
if (!check_all_coordinates(route_parameters.coordinates))
|
|
||||||
{
|
|
||||||
json_result.values["status_message"] = "Invalid coordinates";
|
|
||||||
return Status::Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<double> sub_trace_lengths;
|
|
||||||
const auto &input_coords = route_parameters.coordinates;
|
|
||||||
const auto &input_timestamps = route_parameters.timestamps;
|
|
||||||
const auto &input_bearings = route_parameters.bearings;
|
|
||||||
if (input_timestamps.size() > 0 && input_coords.size() != input_timestamps.size())
|
|
||||||
{
|
|
||||||
json_result.values["status_message"] =
|
|
||||||
"Number of timestamps does not match number of coordinates";
|
|
||||||
return Status::Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input_bearings.size() > 0 && input_coords.size() != input_bearings.size())
|
|
||||||
{
|
|
||||||
json_result.values["status_message"] =
|
|
||||||
"Number of bearings does not match number of coordinates";
|
|
||||||
return Status::Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// at least two coordinates are needed for map matching
|
|
||||||
if (static_cast<int>(input_coords.size()) < 2)
|
|
||||||
{
|
|
||||||
json_result.values["status_message"] = "At least two coordinates needed";
|
|
||||||
return Status::Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto candidates_lists = getCandidates(
|
|
||||||
input_coords, input_bearings, route_parameters.gps_precision, sub_trace_lengths);
|
|
||||||
if (candidates_lists.size() != input_coords.size())
|
|
||||||
{
|
|
||||||
BOOST_ASSERT(candidates_lists.size() < input_coords.size());
|
|
||||||
json_result.values["status_message"] =
|
|
||||||
std::string("Could not find a matching segment for coordinate ") +
|
|
||||||
std::to_string(candidates_lists.size());
|
|
||||||
return Status::NoSegment;
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup logging if enabled
|
|
||||||
if (util::json::Logger::get())
|
|
||||||
util::json::Logger::get()->initialize("matching");
|
|
||||||
|
|
||||||
// call the actual map matching
|
|
||||||
SubMatchingList sub_matchings;
|
|
||||||
search_engine_ptr->map_matching(candidates_lists, input_coords, input_timestamps,
|
|
||||||
route_parameters.matching_beta,
|
|
||||||
route_parameters.gps_precision, sub_matchings);
|
|
||||||
|
|
||||||
util::json::Array matchings;
|
|
||||||
for (auto &sub : sub_matchings)
|
|
||||||
{
|
|
||||||
// classify result
|
|
||||||
if (route_parameters.classify)
|
|
||||||
{
|
|
||||||
double trace_length =
|
|
||||||
sub_trace_lengths[sub.indices.back()] - sub_trace_lengths[sub.indices.front()];
|
|
||||||
TraceClassification classification =
|
|
||||||
classify(trace_length, sub.length,
|
|
||||||
(sub.indices.back() - sub.indices.front() + 1) - sub.nodes.size());
|
|
||||||
if (classification.first == ClassifierT::ClassLabel::POSITIVE)
|
|
||||||
{
|
|
||||||
sub.confidence = classification.second;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sub.confidence = 1 - classification.second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOST_ASSERT(sub.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
|
|
||||||
InternalRouteResult raw_route;
|
|
||||||
PhantomNodes current_phantom_node_pair;
|
|
||||||
for (unsigned i = 0; i < sub.nodes.size() - 1; ++i)
|
|
||||||
{
|
|
||||||
current_phantom_node_pair.source_phantom = sub.nodes[i];
|
|
||||||
current_phantom_node_pair.target_phantom = sub.nodes[i + 1];
|
|
||||||
BOOST_ASSERT(current_phantom_node_pair.source_phantom.IsValid());
|
|
||||||
BOOST_ASSERT(current_phantom_node_pair.target_phantom.IsValid());
|
|
||||||
raw_route.segment_end_coordinates.emplace_back(current_phantom_node_pair);
|
|
||||||
}
|
|
||||||
search_engine_ptr->shortest_path(
|
|
||||||
raw_route.segment_end_coordinates,
|
|
||||||
std::vector<bool>(raw_route.segment_end_coordinates.size() + 1, true), raw_route);
|
|
||||||
|
|
||||||
BOOST_ASSERT(raw_route.shortest_path_length != INVALID_EDGE_WEIGHT);
|
|
||||||
|
|
||||||
matchings.values.emplace_back(submatchingToJSON(sub, route_parameters, raw_route));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (util::json::Logger::get())
|
|
||||||
util::json::Logger::get()->render("matching", json_result);
|
|
||||||
json_result.values["matchings"] = matchings;
|
|
||||||
|
|
||||||
if (sub_matchings.empty())
|
|
||||||
{
|
|
||||||
json_result.values["status_message"] = "Cannot find matchings";
|
|
||||||
return Status::EmptyResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
json_result.values["status_message"] = "Found matchings";
|
|
||||||
return Status::Ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string descriptor_string;
|
|
||||||
DataFacadeT *facade;
|
|
||||||
int max_locations_map_matching;
|
int max_locations_map_matching;
|
||||||
ClassifierT classifier;
|
ClassifierT classifier;
|
||||||
};
|
};
|
||||||
|
388
src/engine/plugins/match.cpp
Normal file
388
src/engine/plugins/match.cpp
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
#ifndef MATCH_HPP
|
||||||
|
#define MATCH_HPP
|
||||||
|
|
||||||
|
#include "engine/plugins/plugin_base.hpp"
|
||||||
|
|
||||||
|
#include "engine/map_matching/bayes_classifier.hpp"
|
||||||
|
#include "engine/object_encoder.hpp"
|
||||||
|
#include "engine/search_engine.hpp"
|
||||||
|
#include "engine/guidance/textual_route_annotation.hpp"
|
||||||
|
#include "engine/guidance/segment_list.hpp"
|
||||||
|
#include "engine/api_response_generator.hpp"
|
||||||
|
#include "engine/routing_algorithms/map_matching.hpp"
|
||||||
|
#include "util/coordinate_calculation.hpp"
|
||||||
|
#include "util/integer_range.hpp"
|
||||||
|
#include "util/json_logger.hpp"
|
||||||
|
#include "util/json_util.hpp"
|
||||||
|
#include "util/string_util.hpp"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace osrm
|
||||||
|
{
|
||||||
|
namespace engine
|
||||||
|
{
|
||||||
|
namespace plugins
|
||||||
|
{
|
||||||
|
|
||||||
|
template <class DataFacadeT> class MapMatchingPlugin : public BasePlugin
|
||||||
|
{
|
||||||
|
std::shared_ptr<SearchEngine<DataFacadeT>> search_engine_ptr;
|
||||||
|
|
||||||
|
using SubMatching = routing_algorithms::SubMatching;
|
||||||
|
using SubMatchingList = routing_algorithms::SubMatchingList;
|
||||||
|
using CandidateLists = routing_algorithms::CandidateLists;
|
||||||
|
using ClassifierT = map_matching::BayesClassifier<map_matching::LaplaceDistribution,
|
||||||
|
map_matching::LaplaceDistribution,
|
||||||
|
double>;
|
||||||
|
using TraceClassification = ClassifierT::ClassificationT;
|
||||||
|
|
||||||
|
public:
|
||||||
|
MapMatchingPlugin(DataFacadeT *facade, const int max_locations_map_matching)
|
||||||
|
: descriptor_string("match"), facade(facade),
|
||||||
|
max_locations_map_matching(max_locations_map_matching),
|
||||||
|
// the values where derived from fitting a laplace distribution
|
||||||
|
// to the values of manually classified traces
|
||||||
|
classifier(map_matching::LaplaceDistribution(0.005986, 0.016646),
|
||||||
|
map_matching::LaplaceDistribution(0.054385, 0.458432),
|
||||||
|
0.696774) // valid apriori probability
|
||||||
|
{
|
||||||
|
search_engine_ptr = std::make_shared<SearchEngine<DataFacadeT>>(facade);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~MapMatchingPlugin() {}
|
||||||
|
|
||||||
|
const std::string GetDescriptor() const final override { return descriptor_string; }
|
||||||
|
|
||||||
|
TraceClassification
|
||||||
|
classify(const float trace_length, const float matched_length, const int removed_points) const
|
||||||
|
{
|
||||||
|
(void)removed_points; // unused
|
||||||
|
|
||||||
|
const double distance_feature = -std::log(trace_length) + std::log(matched_length);
|
||||||
|
|
||||||
|
// matched to the same point
|
||||||
|
if (!std::isfinite(distance_feature))
|
||||||
|
{
|
||||||
|
return std::make_pair(ClassifierT::ClassLabel::NEGATIVE, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto label_with_confidence = classifier.classify(distance_feature);
|
||||||
|
|
||||||
|
return label_with_confidence;
|
||||||
|
}
|
||||||
|
|
||||||
|
CandidateLists getCandidates(
|
||||||
|
const std::vector<util::FixedPointCoordinate> &input_coords,
|
||||||
|
const std::vector<std::pair<const int, const boost::optional<int>>> &input_bearings,
|
||||||
|
const double gps_precision,
|
||||||
|
std::vector<double> &sub_trace_lengths)
|
||||||
|
{
|
||||||
|
CandidateLists candidates_lists;
|
||||||
|
|
||||||
|
// assuming gps_precision is the standard deviation of a normal distribution that
|
||||||
|
// models GPS noise (in this model), this should give us the correct search radius
|
||||||
|
// with > 99% confidence
|
||||||
|
double query_radius = 3 * gps_precision;
|
||||||
|
double last_distance =
|
||||||
|
util::coordinate_calculation::haversineDistance(input_coords[0], input_coords[1]);
|
||||||
|
|
||||||
|
sub_trace_lengths.resize(input_coords.size());
|
||||||
|
sub_trace_lengths[0] = 0;
|
||||||
|
for (const auto current_coordinate : util::irange<std::size_t>(0, input_coords.size()))
|
||||||
|
{
|
||||||
|
bool allow_uturn = false;
|
||||||
|
if (0 < current_coordinate)
|
||||||
|
{
|
||||||
|
last_distance = util::coordinate_calculation::haversineDistance(
|
||||||
|
input_coords[current_coordinate - 1], input_coords[current_coordinate]);
|
||||||
|
|
||||||
|
sub_trace_lengths[current_coordinate] +=
|
||||||
|
sub_trace_lengths[current_coordinate - 1] + last_distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input_coords.size() - 1 > current_coordinate && 0 < current_coordinate)
|
||||||
|
{
|
||||||
|
double turn_angle = util::coordinate_calculation::computeAngle(
|
||||||
|
input_coords[current_coordinate - 1], input_coords[current_coordinate],
|
||||||
|
input_coords[current_coordinate + 1]);
|
||||||
|
|
||||||
|
// sharp turns indicate a possible uturn
|
||||||
|
if (turn_angle <= 90.0 || turn_angle >= 270.0)
|
||||||
|
{
|
||||||
|
allow_uturn = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use bearing values if supplied, otherwise fallback to 0,180 defaults
|
||||||
|
auto bearing = input_bearings.size() > 0 ? input_bearings[current_coordinate].first : 0;
|
||||||
|
auto range = input_bearings.size() > 0
|
||||||
|
? (input_bearings[current_coordinate].second
|
||||||
|
? *input_bearings[current_coordinate].second
|
||||||
|
: 10)
|
||||||
|
: 180;
|
||||||
|
auto candidates = facade->NearestPhantomNodesInRange(input_coords[current_coordinate],
|
||||||
|
query_radius, bearing, range);
|
||||||
|
|
||||||
|
if (candidates.size() == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort by forward id, then by reverse id and then by distance
|
||||||
|
std::sort(
|
||||||
|
candidates.begin(), candidates.end(),
|
||||||
|
[](const PhantomNodeWithDistance &lhs, const PhantomNodeWithDistance &rhs)
|
||||||
|
{
|
||||||
|
return lhs.phantom_node.forward_node_id < rhs.phantom_node.forward_node_id ||
|
||||||
|
(lhs.phantom_node.forward_node_id == rhs.phantom_node.forward_node_id &&
|
||||||
|
(lhs.phantom_node.reverse_node_id < rhs.phantom_node.reverse_node_id ||
|
||||||
|
(lhs.phantom_node.reverse_node_id ==
|
||||||
|
rhs.phantom_node.reverse_node_id &&
|
||||||
|
lhs.distance < rhs.distance)));
|
||||||
|
});
|
||||||
|
|
||||||
|
auto new_end = std::unique(
|
||||||
|
candidates.begin(), candidates.end(),
|
||||||
|
[](const PhantomNodeWithDistance &lhs, const PhantomNodeWithDistance &rhs)
|
||||||
|
{
|
||||||
|
return lhs.phantom_node.forward_node_id == rhs.phantom_node.forward_node_id &&
|
||||||
|
lhs.phantom_node.reverse_node_id == rhs.phantom_node.reverse_node_id;
|
||||||
|
});
|
||||||
|
candidates.resize(new_end - candidates.begin());
|
||||||
|
|
||||||
|
if (!allow_uturn)
|
||||||
|
{
|
||||||
|
const auto compact_size = candidates.size();
|
||||||
|
for (const auto i : util::irange<std::size_t>(0, compact_size))
|
||||||
|
{
|
||||||
|
// Split edge if it is bidirectional and append reverse direction to end of list
|
||||||
|
if (candidates[i].phantom_node.forward_node_id != SPECIAL_NODEID &&
|
||||||
|
candidates[i].phantom_node.reverse_node_id != SPECIAL_NODEID)
|
||||||
|
{
|
||||||
|
PhantomNode reverse_node(candidates[i].phantom_node);
|
||||||
|
reverse_node.forward_node_id = SPECIAL_NODEID;
|
||||||
|
candidates.push_back(
|
||||||
|
PhantomNodeWithDistance{reverse_node, candidates[i].distance});
|
||||||
|
|
||||||
|
candidates[i].phantom_node.reverse_node_id = SPECIAL_NODEID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort by distance to make pruning effective
|
||||||
|
std::sort(candidates.begin(), candidates.end(),
|
||||||
|
[](const PhantomNodeWithDistance &lhs, const PhantomNodeWithDistance &rhs)
|
||||||
|
{
|
||||||
|
return lhs.distance < rhs.distance;
|
||||||
|
});
|
||||||
|
|
||||||
|
candidates_lists.push_back(std::move(candidates));
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates_lists;
|
||||||
|
}
|
||||||
|
|
||||||
|
util::json::Object submatchingToJSON(const SubMatching &sub,
|
||||||
|
const RouteParameters &route_parameters,
|
||||||
|
const InternalRouteResult &raw_route)
|
||||||
|
{
|
||||||
|
util::json::Object subtrace;
|
||||||
|
|
||||||
|
if (route_parameters.classify)
|
||||||
|
{
|
||||||
|
subtrace.values["confidence"] = sub.confidence;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto response_generator = MakeApiResponseGenerator(facade);
|
||||||
|
|
||||||
|
subtrace.values["hint_data"] = response_generator.BuildHintData(raw_route);
|
||||||
|
|
||||||
|
if (route_parameters.geometry || route_parameters.print_instructions)
|
||||||
|
{
|
||||||
|
using SegmentList = guidance::SegmentList<DataFacadeT>;
|
||||||
|
// Passing false to extract_alternative extracts the route.
|
||||||
|
const constexpr bool EXTRACT_ROUTE = false;
|
||||||
|
// by passing false to segment_list, we skip the douglas peucker simplification
|
||||||
|
// and mark all segments as necessary within the generation process
|
||||||
|
const constexpr bool NO_ROUTE_SIMPLIFICATION = false;
|
||||||
|
SegmentList segment_list(raw_route, EXTRACT_ROUTE, route_parameters.zoom_level,
|
||||||
|
NO_ROUTE_SIMPLIFICATION, facade);
|
||||||
|
|
||||||
|
if (route_parameters.geometry)
|
||||||
|
{
|
||||||
|
subtrace.values["geometry"] =
|
||||||
|
response_generator.GetGeometry(route_parameters.compression, segment_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route_parameters.print_instructions)
|
||||||
|
{
|
||||||
|
subtrace.values["instructions"] =
|
||||||
|
guidance::AnnotateRoute<DataFacadeT>(segment_list.Get(), facade);
|
||||||
|
}
|
||||||
|
|
||||||
|
util::json::Object json_route_summary;
|
||||||
|
json_route_summary.values["total_distance"] = segment_list.GetDistance();
|
||||||
|
json_route_summary.values["total_time"] = segment_list.GetDuration();
|
||||||
|
subtrace.values["route_summary"] = json_route_summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
subtrace.values["indices"] = util::json::make_array(sub.indices);
|
||||||
|
|
||||||
|
util::json::Array points;
|
||||||
|
for (const auto &node : sub.nodes)
|
||||||
|
{
|
||||||
|
points.values.emplace_back(
|
||||||
|
util::json::make_array(node.location.lat / COORDINATE_PRECISION,
|
||||||
|
node.location.lon / COORDINATE_PRECISION));
|
||||||
|
}
|
||||||
|
subtrace.values["matched_points"] = points;
|
||||||
|
|
||||||
|
util::json::Array names;
|
||||||
|
for (const auto &node : sub.nodes)
|
||||||
|
{
|
||||||
|
names.values.emplace_back(facade->get_name_for_id(node.name_id));
|
||||||
|
}
|
||||||
|
subtrace.values["matched_names"] = names;
|
||||||
|
|
||||||
|
return subtrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status HandleRequest(const RouteParameters &route_parameters,
|
||||||
|
util::json::Object &json_result) final override
|
||||||
|
{
|
||||||
|
// enforce maximum number of locations for performance reasons
|
||||||
|
if (max_locations_map_matching > 0 &&
|
||||||
|
static_cast<int>(route_parameters.coordinates.size()) > max_locations_map_matching)
|
||||||
|
{
|
||||||
|
json_result.values["status_message"] = "Too many coodindates";
|
||||||
|
return Status::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check number of parameters
|
||||||
|
if (!check_all_coordinates(route_parameters.coordinates))
|
||||||
|
{
|
||||||
|
json_result.values["status_message"] = "Invalid coordinates";
|
||||||
|
return Status::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> sub_trace_lengths;
|
||||||
|
const auto &input_coords = route_parameters.coordinates;
|
||||||
|
const auto &input_timestamps = route_parameters.timestamps;
|
||||||
|
const auto &input_bearings = route_parameters.bearings;
|
||||||
|
if (input_timestamps.size() > 0 && input_coords.size() != input_timestamps.size())
|
||||||
|
{
|
||||||
|
json_result.values["status_message"] =
|
||||||
|
"Number of timestamps does not match number of coordinates";
|
||||||
|
return Status::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input_bearings.size() > 0 && input_coords.size() != input_bearings.size())
|
||||||
|
{
|
||||||
|
json_result.values["status_message"] =
|
||||||
|
"Number of bearings does not match number of coordinates";
|
||||||
|
return Status::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// at least two coordinates are needed for map matching
|
||||||
|
if (static_cast<int>(input_coords.size()) < 2)
|
||||||
|
{
|
||||||
|
json_result.values["status_message"] = "At least two coordinates needed";
|
||||||
|
return Status::Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto candidates_lists = getCandidates(
|
||||||
|
input_coords, input_bearings, route_parameters.gps_precision, sub_trace_lengths);
|
||||||
|
if (candidates_lists.size() != input_coords.size())
|
||||||
|
{
|
||||||
|
BOOST_ASSERT(candidates_lists.size() < input_coords.size());
|
||||||
|
json_result.values["status_message"] =
|
||||||
|
std::string("Could not find a matching segment for coordinate ") +
|
||||||
|
std::to_string(candidates_lists.size());
|
||||||
|
return Status::NoSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup logging if enabled
|
||||||
|
if (util::json::Logger::get())
|
||||||
|
util::json::Logger::get()->initialize("matching");
|
||||||
|
|
||||||
|
// call the actual map matching
|
||||||
|
SubMatchingList sub_matchings;
|
||||||
|
search_engine_ptr->map_matching(candidates_lists, input_coords, input_timestamps,
|
||||||
|
route_parameters.matching_beta,
|
||||||
|
route_parameters.gps_precision, sub_matchings);
|
||||||
|
|
||||||
|
util::json::Array matchings;
|
||||||
|
for (auto &sub : sub_matchings)
|
||||||
|
{
|
||||||
|
// classify result
|
||||||
|
if (route_parameters.classify)
|
||||||
|
{
|
||||||
|
double trace_length =
|
||||||
|
sub_trace_lengths[sub.indices.back()] - sub_trace_lengths[sub.indices.front()];
|
||||||
|
TraceClassification classification =
|
||||||
|
classify(trace_length, sub.length,
|
||||||
|
(sub.indices.back() - sub.indices.front() + 1) - sub.nodes.size());
|
||||||
|
if (classification.first == ClassifierT::ClassLabel::POSITIVE)
|
||||||
|
{
|
||||||
|
sub.confidence = classification.second;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sub.confidence = 1 - classification.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOST_ASSERT(sub.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
|
||||||
|
InternalRouteResult raw_route;
|
||||||
|
PhantomNodes current_phantom_node_pair;
|
||||||
|
for (unsigned i = 0; i < sub.nodes.size() - 1; ++i)
|
||||||
|
{
|
||||||
|
current_phantom_node_pair.source_phantom = sub.nodes[i];
|
||||||
|
current_phantom_node_pair.target_phantom = sub.nodes[i + 1];
|
||||||
|
BOOST_ASSERT(current_phantom_node_pair.source_phantom.IsValid());
|
||||||
|
BOOST_ASSERT(current_phantom_node_pair.target_phantom.IsValid());
|
||||||
|
raw_route.segment_end_coordinates.emplace_back(current_phantom_node_pair);
|
||||||
|
}
|
||||||
|
search_engine_ptr->shortest_path(
|
||||||
|
raw_route.segment_end_coordinates,
|
||||||
|
std::vector<bool>(raw_route.segment_end_coordinates.size() + 1, true), raw_route);
|
||||||
|
|
||||||
|
BOOST_ASSERT(raw_route.shortest_path_length != INVALID_EDGE_WEIGHT);
|
||||||
|
|
||||||
|
matchings.values.emplace_back(submatchingToJSON(sub, route_parameters, raw_route));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (util::json::Logger::get())
|
||||||
|
util::json::Logger::get()->render("matching", json_result);
|
||||||
|
json_result.values["matchings"] = matchings;
|
||||||
|
|
||||||
|
if (sub_matchings.empty())
|
||||||
|
{
|
||||||
|
json_result.values["status_message"] = "Cannot find matchings";
|
||||||
|
return Status::EmptyResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
json_result.values["status_message"] = "Found matchings";
|
||||||
|
return Status::Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string descriptor_string;
|
||||||
|
DataFacadeT *facade;
|
||||||
|
int max_locations_map_matching;
|
||||||
|
ClassifierT classifier;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // MATCH_HPP
|
Loading…
Reference in New Issue
Block a user