2015-12-03 14:04:23 -05:00
|
|
|
#ifndef GEOSPATIAL_QUERY_HPP
|
|
|
|
#define GEOSPATIAL_QUERY_HPP
|
|
|
|
|
2017-05-22 10:07:12 -04:00
|
|
|
#include "engine/approach.hpp"
|
2017-05-29 08:46:11 -04:00
|
|
|
#include "engine/phantom_node.hpp"
|
2016-01-02 11:13:44 -05:00
|
|
|
#include "util/bearing.hpp"
|
2016-05-27 15:05:04 -04:00
|
|
|
#include "util/coordinate_calculation.hpp"
|
2016-02-16 13:51:04 -05:00
|
|
|
#include "util/rectangle.hpp"
|
2016-05-27 15:05:04 -04:00
|
|
|
#include "util/typedefs.hpp"
|
2016-04-08 20:18:47 -04:00
|
|
|
#include "util/web_mercator.hpp"
|
2015-12-03 14:04:23 -05:00
|
|
|
|
2016-01-02 11:13:44 -05:00
|
|
|
#include "osrm/coordinate.hpp"
|
2015-12-03 14:04:23 -05:00
|
|
|
|
|
|
|
#include <algorithm>
|
2016-01-07 05:35:35 -05:00
|
|
|
#include <cmath>
|
|
|
|
#include <memory>
|
|
|
|
#include <vector>
|
2015-12-03 14:04:23 -05:00
|
|
|
|
2016-01-05 10:51:13 -05:00
|
|
|
namespace osrm
|
|
|
|
{
|
|
|
|
namespace engine
|
|
|
|
{
|
|
|
|
|
2016-07-22 03:08:40 -04:00
|
|
|
inline std::pair<bool, bool> boolPairAnd(const std::pair<bool, bool> &A,
|
|
|
|
const std::pair<bool, bool> &B)
|
2016-07-28 17:09:55 -04:00
|
|
|
{
|
|
|
|
return std::make_pair(A.first && B.first, A.second && B.second);
|
|
|
|
}
|
|
|
|
|
2015-12-03 14:04:23 -05:00
|
|
|
// Implements complex queries on top of an RTree and builds PhantomNodes from it.
|
|
|
|
//
|
2016-05-06 20:43:37 -04:00
|
|
|
// Only holds a weak reference on the RTree and coordinates!
|
2016-01-29 20:52:20 -05:00
|
|
|
template <typename RTreeT, typename DataFacadeT> class GeospatialQuery
|
2015-12-03 14:04:23 -05:00
|
|
|
{
|
|
|
|
using EdgeData = typename RTreeT::EdgeData;
|
|
|
|
using CoordinateList = typename RTreeT::CoordinateList;
|
2016-03-28 14:38:19 -04:00
|
|
|
using CandidateSegment = typename RTreeT::CandidateSegment;
|
2015-12-03 14:04:23 -05:00
|
|
|
|
|
|
|
public:
|
2016-05-27 15:05:04 -04:00
|
|
|
GeospatialQuery(RTreeT &rtree_, const CoordinateList &coordinates_, DataFacadeT &datafacade_)
|
2016-05-06 20:43:37 -04:00
|
|
|
: rtree(rtree_), coordinates(coordinates_), datafacade(datafacade_)
|
2015-12-03 14:04:23 -05:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2016-02-26 06:29:57 -05:00
|
|
|
std::vector<EdgeData> Search(const util::RectangleInt2D &bbox)
|
2016-02-16 13:51:04 -05:00
|
|
|
{
|
|
|
|
return rtree.SearchInBox(bbox);
|
|
|
|
}
|
|
|
|
|
2016-02-20 22:27:26 -05:00
|
|
|
// Returns nearest PhantomNodes in the given bearing range within max_distance.
|
|
|
|
// Does not filter by small/big component!
|
|
|
|
std::vector<PhantomNodeWithDistance>
|
2016-05-27 15:05:04 -04:00
|
|
|
NearestPhantomNodesInRange(const util::Coordinate input_coordinate,
|
2017-05-23 06:23:22 -04:00
|
|
|
const double max_distance,
|
2017-05-29 10:13:15 -04:00
|
|
|
const Approach approach) const
|
2016-02-20 22:27:26 -05:00
|
|
|
{
|
2017-05-29 08:46:11 -04:00
|
|
|
auto results = rtree.Nearest(
|
|
|
|
input_coordinate,
|
|
|
|
[this, approach, &input_coordinate](const CandidateSegment &segment) {
|
2017-08-16 16:21:19 -04:00
|
|
|
return boolPairAnd(boolPairAnd(HasValidEdge(segment), CheckSegmentExclude(segment)),
|
2017-05-29 10:13:15 -04:00
|
|
|
CheckApproach(input_coordinate, segment, approach));
|
2017-05-29 08:46:11 -04:00
|
|
|
},
|
|
|
|
[this, max_distance, input_coordinate](const std::size_t,
|
|
|
|
const CandidateSegment &segment) {
|
|
|
|
return CheckSegmentDistance(input_coordinate, segment, max_distance);
|
|
|
|
});
|
2016-02-20 22:27:26 -05:00
|
|
|
|
|
|
|
return MakePhantomNodes(input_coordinate, results);
|
|
|
|
}
|
|
|
|
|
2015-12-03 14:04:23 -05:00
|
|
|
// Returns nearest PhantomNodes in the given bearing range within max_distance.
|
|
|
|
// Does not filter by small/big component!
|
2015-12-09 16:34:22 -05:00
|
|
|
std::vector<PhantomNodeWithDistance>
|
2016-02-23 15:23:13 -05:00
|
|
|
NearestPhantomNodesInRange(const util::Coordinate input_coordinate,
|
2015-12-26 14:12:10 -05:00
|
|
|
const double max_distance,
|
2016-02-20 22:27:26 -05:00
|
|
|
const int bearing,
|
2017-05-23 06:23:22 -04:00
|
|
|
const int bearing_range,
|
2017-05-29 10:13:15 -04:00
|
|
|
const Approach approach) const
|
2015-12-03 14:04:23 -05:00
|
|
|
{
|
2016-03-28 14:38:19 -04:00
|
|
|
auto results = rtree.Nearest(
|
|
|
|
input_coordinate,
|
2018-02-02 06:59:30 -05:00
|
|
|
[this, approach, &input_coordinate, bearing, bearing_range](
|
2017-05-29 08:46:11 -04:00
|
|
|
const CandidateSegment &segment) {
|
2017-07-28 09:59:48 -04:00
|
|
|
auto use_direction =
|
|
|
|
boolPairAnd(CheckSegmentBearing(segment, bearing, bearing_range),
|
2017-08-16 16:21:19 -04:00
|
|
|
boolPairAnd(HasValidEdge(segment), CheckSegmentExclude(segment)));
|
2017-05-29 08:46:11 -04:00
|
|
|
use_direction =
|
2017-05-29 10:13:15 -04:00
|
|
|
boolPairAnd(use_direction, CheckApproach(input_coordinate, segment, approach));
|
2017-05-23 06:23:22 -04:00
|
|
|
return use_direction;
|
2016-03-28 14:38:19 -04:00
|
|
|
},
|
|
|
|
[this, max_distance, input_coordinate](const std::size_t,
|
2016-05-27 15:05:04 -04:00
|
|
|
const CandidateSegment &segment) {
|
2016-04-28 18:14:44 -04:00
|
|
|
return CheckSegmentDistance(input_coordinate, segment, max_distance);
|
2016-03-28 14:38:19 -04:00
|
|
|
});
|
2015-12-03 14:04:23 -05:00
|
|
|
|
|
|
|
return MakePhantomNodes(input_coordinate, results);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns max_results nearest PhantomNodes in the given bearing range.
|
|
|
|
// Does not filter by small/big component!
|
2015-12-09 16:34:22 -05:00
|
|
|
std::vector<PhantomNodeWithDistance>
|
2016-02-23 15:23:13 -05:00
|
|
|
NearestPhantomNodes(const util::Coordinate input_coordinate,
|
2015-12-03 14:04:23 -05:00
|
|
|
const unsigned max_results,
|
2016-02-22 18:44:35 -05:00
|
|
|
const int bearing,
|
2017-05-23 06:23:22 -04:00
|
|
|
const int bearing_range,
|
2017-05-29 10:13:15 -04:00
|
|
|
const Approach approach) const
|
2015-12-03 14:04:23 -05:00
|
|
|
{
|
2016-07-29 01:13:17 -04:00
|
|
|
auto results = rtree.Nearest(
|
|
|
|
input_coordinate,
|
2017-05-29 08:46:11 -04:00
|
|
|
[this, approach, &input_coordinate, bearing, bearing_range](
|
|
|
|
const CandidateSegment &segment) {
|
2017-07-28 09:59:48 -04:00
|
|
|
auto use_direction =
|
|
|
|
boolPairAnd(CheckSegmentBearing(segment, bearing, bearing_range),
|
2017-08-16 16:21:19 -04:00
|
|
|
boolPairAnd(HasValidEdge(segment), CheckSegmentExclude(segment)));
|
2017-05-29 08:46:11 -04:00
|
|
|
return boolPairAnd(use_direction,
|
2017-05-29 10:13:15 -04:00
|
|
|
CheckApproach(input_coordinate, segment, approach));
|
2016-07-29 01:13:17 -04:00
|
|
|
},
|
|
|
|
[max_results](const std::size_t num_results, const CandidateSegment &) {
|
|
|
|
return num_results >= max_results;
|
|
|
|
});
|
2015-12-03 14:04:23 -05:00
|
|
|
|
|
|
|
return MakePhantomNodes(input_coordinate, results);
|
|
|
|
}
|
|
|
|
|
2016-02-23 15:23:13 -05:00
|
|
|
// Returns max_results nearest PhantomNodes in the given bearing range within the maximum
|
|
|
|
// distance.
|
2016-02-22 18:44:35 -05:00
|
|
|
// Does not filter by small/big component!
|
|
|
|
std::vector<PhantomNodeWithDistance>
|
2016-02-23 15:23:13 -05:00
|
|
|
NearestPhantomNodes(const util::Coordinate input_coordinate,
|
2016-02-22 18:44:35 -05:00
|
|
|
const unsigned max_results,
|
|
|
|
const double max_distance,
|
|
|
|
const int bearing,
|
2017-05-23 06:23:22 -04:00
|
|
|
const int bearing_range,
|
2017-05-29 10:13:15 -04:00
|
|
|
const Approach approach) const
|
2016-02-22 18:44:35 -05:00
|
|
|
{
|
2016-07-29 01:13:17 -04:00
|
|
|
auto results = rtree.Nearest(
|
|
|
|
input_coordinate,
|
2017-05-29 08:46:11 -04:00
|
|
|
[this, approach, &input_coordinate, bearing, bearing_range](
|
|
|
|
const CandidateSegment &segment) {
|
2017-07-28 09:59:48 -04:00
|
|
|
auto use_direction =
|
|
|
|
boolPairAnd(CheckSegmentBearing(segment, bearing, bearing_range),
|
2017-08-16 16:21:19 -04:00
|
|
|
boolPairAnd(HasValidEdge(segment), CheckSegmentExclude(segment)));
|
2017-05-29 08:46:11 -04:00
|
|
|
return boolPairAnd(use_direction,
|
2017-05-29 10:13:15 -04:00
|
|
|
CheckApproach(input_coordinate, segment, approach));
|
2016-07-29 01:13:17 -04:00
|
|
|
},
|
|
|
|
[this, max_distance, max_results, input_coordinate](const std::size_t num_results,
|
|
|
|
const CandidateSegment &segment) {
|
|
|
|
return num_results >= max_results ||
|
|
|
|
CheckSegmentDistance(input_coordinate, segment, max_distance);
|
|
|
|
});
|
2016-02-22 18:44:35 -05:00
|
|
|
|
|
|
|
return MakePhantomNodes(input_coordinate, results);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns max_results nearest PhantomNodes.
|
|
|
|
// Does not filter by small/big component!
|
|
|
|
std::vector<PhantomNodeWithDistance>
|
2017-05-23 06:23:22 -04:00
|
|
|
NearestPhantomNodes(const util::Coordinate input_coordinate,
|
|
|
|
const unsigned max_results,
|
2017-05-29 10:13:15 -04:00
|
|
|
const Approach approach) const
|
2016-02-22 18:44:35 -05:00
|
|
|
{
|
2017-05-29 08:46:11 -04:00
|
|
|
auto results = rtree.Nearest(
|
|
|
|
input_coordinate,
|
|
|
|
[this, approach, &input_coordinate](const CandidateSegment &segment) {
|
2017-08-16 16:21:19 -04:00
|
|
|
return boolPairAnd(boolPairAnd(HasValidEdge(segment), CheckSegmentExclude(segment)),
|
2017-05-29 10:13:15 -04:00
|
|
|
CheckApproach(input_coordinate, segment, approach));
|
2017-05-29 08:46:11 -04:00
|
|
|
},
|
|
|
|
[max_results](const std::size_t num_results, const CandidateSegment &) {
|
|
|
|
return num_results >= max_results;
|
|
|
|
});
|
2016-02-22 18:44:35 -05:00
|
|
|
|
|
|
|
return MakePhantomNodes(input_coordinate, results);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns max_results nearest PhantomNodes in the given max distance.
|
|
|
|
// Does not filter by small/big component!
|
|
|
|
std::vector<PhantomNodeWithDistance>
|
2016-02-23 15:23:13 -05:00
|
|
|
NearestPhantomNodes(const util::Coordinate input_coordinate,
|
2016-02-22 18:44:35 -05:00
|
|
|
const unsigned max_results,
|
2017-05-23 06:23:22 -04:00
|
|
|
const double max_distance,
|
2017-05-29 10:13:15 -04:00
|
|
|
const Approach approach) const
|
2016-02-22 18:44:35 -05:00
|
|
|
{
|
2017-05-29 08:46:11 -04:00
|
|
|
auto results = rtree.Nearest(
|
|
|
|
input_coordinate,
|
|
|
|
[this, approach, &input_coordinate](const CandidateSegment &segment) {
|
2017-08-16 16:21:19 -04:00
|
|
|
return boolPairAnd(boolPairAnd(HasValidEdge(segment), CheckSegmentExclude(segment)),
|
2017-05-29 10:13:15 -04:00
|
|
|
CheckApproach(input_coordinate, segment, approach));
|
2017-05-29 08:46:11 -04:00
|
|
|
},
|
|
|
|
[this, max_distance, max_results, input_coordinate](const std::size_t num_results,
|
|
|
|
const CandidateSegment &segment) {
|
|
|
|
return num_results >= max_results ||
|
|
|
|
CheckSegmentDistance(input_coordinate, segment, max_distance);
|
|
|
|
});
|
2016-02-22 18:44:35 -05:00
|
|
|
|
|
|
|
return MakePhantomNodes(input_coordinate, results);
|
|
|
|
}
|
|
|
|
|
2016-01-28 10:28:44 -05:00
|
|
|
// Returns the nearest phantom node. If this phantom node is not from a big component
|
|
|
|
// a second phantom node is return that is the nearest coordinate in a big component.
|
2016-02-23 15:23:13 -05:00
|
|
|
std::pair<PhantomNode, PhantomNode>
|
|
|
|
NearestPhantomNodeWithAlternativeFromBigComponent(const util::Coordinate input_coordinate,
|
2017-05-23 06:23:22 -04:00
|
|
|
const double max_distance,
|
2017-05-29 10:13:15 -04:00
|
|
|
const Approach approach) const
|
2016-01-28 10:28:44 -05:00
|
|
|
{
|
|
|
|
bool has_small_component = false;
|
|
|
|
bool has_big_component = false;
|
|
|
|
auto results = rtree.Nearest(
|
|
|
|
input_coordinate,
|
2017-05-29 08:46:11 -04:00
|
|
|
[this, approach, &input_coordinate, &has_big_component, &has_small_component](
|
|
|
|
const CandidateSegment &segment) {
|
2017-05-15 06:15:00 -04:00
|
|
|
auto use_segment =
|
|
|
|
(!has_small_component || (!has_big_component && !IsTinyComponent(segment)));
|
2016-01-28 10:28:44 -05:00
|
|
|
auto use_directions = std::make_pair(use_segment, use_segment);
|
2016-07-28 17:09:55 -04:00
|
|
|
const auto valid_edges = HasValidEdge(segment);
|
2017-08-16 16:21:19 -04:00
|
|
|
const auto admissible_segments = CheckSegmentExclude(segment);
|
2017-07-28 09:59:48 -04:00
|
|
|
use_directions = boolPairAnd(use_directions, admissible_segments);
|
2017-05-23 06:23:22 -04:00
|
|
|
use_directions = boolPairAnd(use_directions, valid_edges);
|
2017-05-29 10:13:15 -04:00
|
|
|
use_directions =
|
|
|
|
boolPairAnd(use_directions, CheckApproach(input_coordinate, segment, approach));
|
2016-01-28 10:28:44 -05:00
|
|
|
|
2017-05-23 06:23:22 -04:00
|
|
|
if (use_directions.first || use_directions.second)
|
2016-07-28 17:09:55 -04:00
|
|
|
{
|
2017-05-15 06:15:00 -04:00
|
|
|
has_big_component = has_big_component || !IsTinyComponent(segment);
|
|
|
|
has_small_component = has_small_component || IsTinyComponent(segment);
|
2016-07-28 17:09:55 -04:00
|
|
|
}
|
2017-05-23 06:23:22 -04:00
|
|
|
|
2016-01-28 10:28:44 -05:00
|
|
|
return use_directions;
|
|
|
|
},
|
2016-05-27 15:05:04 -04:00
|
|
|
[this, &has_big_component, max_distance, input_coordinate](
|
|
|
|
const std::size_t num_results, const CandidateSegment &segment) {
|
2016-03-28 14:38:19 -04:00
|
|
|
return (num_results > 0 && has_big_component) ||
|
2016-04-28 18:14:44 -04:00
|
|
|
CheckSegmentDistance(input_coordinate, segment, max_distance);
|
2016-01-28 10:28:44 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
if (results.size() == 0)
|
|
|
|
{
|
|
|
|
return std::make_pair(PhantomNode{}, PhantomNode{});
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOST_ASSERT(results.size() == 1 || results.size() == 2);
|
|
|
|
return std::make_pair(MakePhantomNode(input_coordinate, results.front()).phantom_node,
|
|
|
|
MakePhantomNode(input_coordinate, results.back()).phantom_node);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns the nearest phantom node. If this phantom node is not from a big component
|
|
|
|
// a second phantom node is return that is the nearest coordinate in a big component.
|
2016-02-23 15:23:13 -05:00
|
|
|
std::pair<PhantomNode, PhantomNode>
|
2017-05-18 11:05:11 -04:00
|
|
|
NearestPhantomNodeWithAlternativeFromBigComponent(const util::Coordinate input_coordinate,
|
2017-05-29 10:13:15 -04:00
|
|
|
const Approach approach) const
|
2016-01-28 10:28:44 -05:00
|
|
|
{
|
|
|
|
bool has_small_component = false;
|
|
|
|
bool has_big_component = false;
|
2016-03-28 14:38:19 -04:00
|
|
|
auto results = rtree.Nearest(
|
|
|
|
input_coordinate,
|
2017-05-23 06:23:22 -04:00
|
|
|
[this, approach, &input_coordinate, &has_big_component, &has_small_component](
|
2017-05-22 06:10:43 -04:00
|
|
|
const CandidateSegment &segment) {
|
2017-05-15 06:15:00 -04:00
|
|
|
auto use_segment =
|
|
|
|
(!has_small_component || (!has_big_component && !IsTinyComponent(segment)));
|
2016-03-28 14:38:19 -04:00
|
|
|
auto use_directions = std::make_pair(use_segment, use_segment);
|
2017-05-18 11:05:11 -04:00
|
|
|
|
2016-07-28 17:09:55 -04:00
|
|
|
const auto valid_edges = HasValidEdge(segment);
|
2017-08-16 16:21:19 -04:00
|
|
|
const auto admissible_segments = CheckSegmentExclude(segment);
|
2017-07-28 09:59:48 -04:00
|
|
|
use_directions = boolPairAnd(use_directions, admissible_segments);
|
2017-05-23 06:23:22 -04:00
|
|
|
use_directions = boolPairAnd(use_directions, valid_edges);
|
2017-05-29 10:13:15 -04:00
|
|
|
use_directions =
|
|
|
|
boolPairAnd(use_directions, CheckApproach(input_coordinate, segment, approach));
|
2016-07-28 17:09:55 -04:00
|
|
|
|
2017-05-23 06:23:22 -04:00
|
|
|
if (use_directions.first || use_directions.second)
|
2016-07-28 17:09:55 -04:00
|
|
|
{
|
2017-05-15 06:15:00 -04:00
|
|
|
has_big_component = has_big_component || !IsTinyComponent(segment);
|
|
|
|
has_small_component = has_small_component || IsTinyComponent(segment);
|
2016-07-28 17:09:55 -04:00
|
|
|
}
|
2016-01-28 10:28:44 -05:00
|
|
|
|
2016-03-28 14:38:19 -04:00
|
|
|
return use_directions;
|
|
|
|
},
|
2016-05-27 15:05:04 -04:00
|
|
|
[&has_big_component](const std::size_t num_results, const CandidateSegment &) {
|
2016-03-28 14:38:19 -04:00
|
|
|
return num_results > 0 && has_big_component;
|
|
|
|
});
|
2016-01-28 10:28:44 -05:00
|
|
|
|
|
|
|
if (results.size() == 0)
|
|
|
|
{
|
|
|
|
return std::make_pair(PhantomNode{}, PhantomNode{});
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOST_ASSERT(results.size() == 1 || results.size() == 2);
|
|
|
|
return std::make_pair(MakePhantomNode(input_coordinate, results.front()).phantom_node,
|
|
|
|
MakePhantomNode(input_coordinate, results.back()).phantom_node);
|
|
|
|
}
|
|
|
|
|
2015-12-03 14:04:23 -05:00
|
|
|
// Returns the nearest phantom node. If this phantom node is not from a big component
|
|
|
|
// a second phantom node is return that is the nearest coordinate in a big component.
|
2017-05-29 08:46:11 -04:00
|
|
|
std::pair<PhantomNode, PhantomNode>
|
|
|
|
NearestPhantomNodeWithAlternativeFromBigComponent(const util::Coordinate input_coordinate,
|
|
|
|
const int bearing,
|
|
|
|
const int bearing_range,
|
2017-05-29 10:13:15 -04:00
|
|
|
const Approach approach) const
|
2015-12-03 14:04:23 -05:00
|
|
|
{
|
|
|
|
bool has_small_component = false;
|
|
|
|
bool has_big_component = false;
|
|
|
|
auto results = rtree.Nearest(
|
|
|
|
input_coordinate,
|
2017-05-29 08:46:11 -04:00
|
|
|
[this,
|
|
|
|
approach,
|
|
|
|
&input_coordinate,
|
|
|
|
bearing,
|
|
|
|
bearing_range,
|
|
|
|
&has_big_component,
|
|
|
|
&has_small_component](const CandidateSegment &segment) {
|
2017-05-15 06:15:00 -04:00
|
|
|
auto use_segment =
|
|
|
|
(!has_small_component || (!has_big_component && !IsTinyComponent(segment)));
|
2015-12-03 14:04:23 -05:00
|
|
|
auto use_directions = std::make_pair(use_segment, use_segment);
|
2017-08-16 16:21:19 -04:00
|
|
|
const auto admissible_segments = CheckSegmentExclude(segment);
|
2016-07-28 17:09:55 -04:00
|
|
|
use_directions = boolPairAnd(use_directions, HasValidEdge(segment));
|
2015-12-03 14:04:23 -05:00
|
|
|
|
|
|
|
if (use_segment)
|
|
|
|
{
|
2016-07-29 01:13:17 -04:00
|
|
|
use_directions =
|
|
|
|
boolPairAnd(CheckSegmentBearing(segment, bearing, bearing_range),
|
|
|
|
HasValidEdge(segment));
|
2017-07-28 09:59:48 -04:00
|
|
|
use_directions = boolPairAnd(use_directions, admissible_segments);
|
2017-05-29 08:46:11 -04:00
|
|
|
use_directions = boolPairAnd(
|
2017-05-29 10:13:15 -04:00
|
|
|
use_directions, CheckApproach(input_coordinate, segment, approach));
|
2017-05-23 06:23:22 -04:00
|
|
|
|
2015-12-03 14:04:23 -05:00
|
|
|
if (use_directions.first || use_directions.second)
|
|
|
|
{
|
2017-05-15 06:15:00 -04:00
|
|
|
has_big_component = has_big_component || !IsTinyComponent(segment);
|
|
|
|
has_small_component = has_small_component || IsTinyComponent(segment);
|
2015-12-03 14:04:23 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return use_directions;
|
|
|
|
},
|
2016-05-27 15:05:04 -04:00
|
|
|
[&has_big_component](const std::size_t num_results, const CandidateSegment &) {
|
2015-12-03 14:04:23 -05:00
|
|
|
return num_results > 0 && has_big_component;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (results.size() == 0)
|
|
|
|
{
|
2015-12-09 16:34:22 -05:00
|
|
|
return std::make_pair(PhantomNode{}, PhantomNode{});
|
2015-12-03 14:04:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
BOOST_ASSERT(results.size() > 0);
|
2015-12-09 16:34:22 -05:00
|
|
|
return std::make_pair(MakePhantomNode(input_coordinate, results.front()).phantom_node,
|
|
|
|
MakePhantomNode(input_coordinate, results.back()).phantom_node);
|
2015-12-03 14:04:23 -05:00
|
|
|
}
|
|
|
|
|
2016-01-28 10:28:44 -05:00
|
|
|
// Returns the nearest phantom node. If this phantom node is not from a big component
|
|
|
|
// a second phantom node is return that is the nearest coordinate in a big component.
|
2016-02-23 15:23:13 -05:00
|
|
|
std::pair<PhantomNode, PhantomNode>
|
|
|
|
NearestPhantomNodeWithAlternativeFromBigComponent(const util::Coordinate input_coordinate,
|
|
|
|
const double max_distance,
|
|
|
|
const int bearing,
|
2017-05-23 06:23:22 -04:00
|
|
|
const int bearing_range,
|
2017-05-29 10:13:15 -04:00
|
|
|
const Approach approach) const
|
2016-01-28 10:28:44 -05:00
|
|
|
{
|
|
|
|
bool has_small_component = false;
|
|
|
|
bool has_big_component = false;
|
|
|
|
auto results = rtree.Nearest(
|
|
|
|
input_coordinate,
|
2017-05-29 08:46:11 -04:00
|
|
|
[this,
|
|
|
|
approach,
|
|
|
|
&input_coordinate,
|
|
|
|
bearing,
|
|
|
|
bearing_range,
|
|
|
|
&has_big_component,
|
|
|
|
&has_small_component](const CandidateSegment &segment) {
|
2017-05-15 06:15:00 -04:00
|
|
|
auto use_segment =
|
|
|
|
(!has_small_component || (!has_big_component && !IsTinyComponent(segment)));
|
2016-01-28 10:28:44 -05:00
|
|
|
auto use_directions = std::make_pair(use_segment, use_segment);
|
2017-08-16 16:21:19 -04:00
|
|
|
const auto admissible_segments = CheckSegmentExclude(segment);
|
2016-07-28 17:09:55 -04:00
|
|
|
use_directions = boolPairAnd(use_directions, HasValidEdge(segment));
|
2016-01-28 10:28:44 -05:00
|
|
|
|
|
|
|
if (use_segment)
|
|
|
|
{
|
2016-07-29 01:13:17 -04:00
|
|
|
use_directions =
|
|
|
|
boolPairAnd(CheckSegmentBearing(segment, bearing, bearing_range),
|
|
|
|
HasValidEdge(segment));
|
2017-07-28 09:59:48 -04:00
|
|
|
use_directions = boolPairAnd(use_directions, admissible_segments);
|
2017-05-29 08:46:11 -04:00
|
|
|
use_directions = boolPairAnd(
|
2017-05-29 10:13:15 -04:00
|
|
|
use_directions, CheckApproach(input_coordinate, segment, approach));
|
2017-05-23 06:23:22 -04:00
|
|
|
|
2016-01-28 10:28:44 -05:00
|
|
|
if (use_directions.first || use_directions.second)
|
|
|
|
{
|
2017-05-15 06:15:00 -04:00
|
|
|
has_big_component = has_big_component || !IsTinyComponent(segment);
|
|
|
|
has_small_component = has_small_component || IsTinyComponent(segment);
|
2016-01-28 10:28:44 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return use_directions;
|
|
|
|
},
|
2016-05-27 15:05:04 -04:00
|
|
|
[this, &has_big_component, max_distance, input_coordinate](
|
|
|
|
const std::size_t num_results, const CandidateSegment &segment) {
|
2016-03-28 14:38:19 -04:00
|
|
|
return (num_results > 0 && has_big_component) ||
|
2016-04-28 18:14:44 -04:00
|
|
|
CheckSegmentDistance(input_coordinate, segment, max_distance);
|
2016-01-28 10:28:44 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
if (results.size() == 0)
|
|
|
|
{
|
|
|
|
return std::make_pair(PhantomNode{}, PhantomNode{});
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOST_ASSERT(results.size() > 0);
|
|
|
|
return std::make_pair(MakePhantomNode(input_coordinate, results.front()).phantom_node,
|
|
|
|
MakePhantomNode(input_coordinate, results.back()).phantom_node);
|
|
|
|
}
|
|
|
|
|
2015-12-03 14:04:23 -05:00
|
|
|
private:
|
2015-12-09 16:34:22 -05:00
|
|
|
std::vector<PhantomNodeWithDistance>
|
2016-02-23 15:23:13 -05:00
|
|
|
MakePhantomNodes(const util::Coordinate input_coordinate,
|
2015-12-03 14:04:23 -05:00
|
|
|
const std::vector<EdgeData> &results) const
|
|
|
|
{
|
2015-12-09 16:34:22 -05:00
|
|
|
std::vector<PhantomNodeWithDistance> distance_and_phantoms(results.size());
|
2016-05-27 15:05:04 -04:00
|
|
|
std::transform(results.begin(),
|
|
|
|
results.end(),
|
|
|
|
distance_and_phantoms.begin(),
|
|
|
|
[this, &input_coordinate](const EdgeData &data) {
|
2015-12-03 14:04:23 -05:00
|
|
|
return MakePhantomNode(input_coordinate, data);
|
|
|
|
});
|
|
|
|
return distance_and_phantoms;
|
|
|
|
}
|
|
|
|
|
2016-02-23 15:23:13 -05:00
|
|
|
PhantomNodeWithDistance MakePhantomNode(const util::Coordinate input_coordinate,
|
2016-01-05 06:04:04 -05:00
|
|
|
const EdgeData &data) const
|
2015-12-03 14:04:23 -05:00
|
|
|
{
|
2016-02-23 15:23:13 -05:00
|
|
|
util::Coordinate point_on_segment;
|
2015-12-26 14:12:10 -05:00
|
|
|
double ratio;
|
2016-01-07 19:31:57 -05:00
|
|
|
const auto current_perpendicular_distance =
|
2016-05-27 15:05:04 -04:00
|
|
|
util::coordinate_calculation::perpendicularDistance(coordinates[data.u],
|
|
|
|
coordinates[data.v],
|
|
|
|
input_coordinate,
|
|
|
|
point_on_segment,
|
|
|
|
ratio);
|
2015-12-03 14:04:23 -05:00
|
|
|
|
2016-01-29 20:52:20 -05:00
|
|
|
// Find the node-based-edge that this belongs to, and directly
|
|
|
|
// calculate the forward_weight, forward_offset, reverse_weight, reverse_offset
|
2015-12-03 14:04:23 -05:00
|
|
|
|
2017-05-11 03:13:59 -04:00
|
|
|
BOOST_ASSERT(data.forward_segment_id.enabled || data.reverse_segment_id.enabled);
|
|
|
|
BOOST_ASSERT(!data.reverse_segment_id.enabled ||
|
|
|
|
datafacade.GetGeometryIndex(data.forward_segment_id.id).id ==
|
|
|
|
datafacade.GetGeometryIndex(data.reverse_segment_id.id).id);
|
|
|
|
const auto geometry_id = datafacade.GetGeometryIndex(data.forward_segment_id.id).id;
|
2017-05-15 06:15:00 -04:00
|
|
|
const auto component_id = datafacade.GetComponentID(data.forward_segment_id.id);
|
2017-05-11 03:13:59 -04:00
|
|
|
|
2018-03-19 14:41:02 -04:00
|
|
|
const auto forward_weights = datafacade.GetUncompressedForwardWeights(geometry_id);
|
|
|
|
const auto reverse_weights = datafacade.GetUncompressedReverseWeights(geometry_id);
|
2016-07-22 12:23:54 -04:00
|
|
|
|
2018-03-19 14:41:02 -04:00
|
|
|
const auto forward_durations = datafacade.GetUncompressedForwardDurations(geometry_id);
|
|
|
|
const auto reverse_durations = datafacade.GetUncompressedReverseDurations(geometry_id);
|
2016-07-22 12:23:54 -04:00
|
|
|
|
2018-03-19 14:41:02 -04:00
|
|
|
const auto forward_weight_offset =
|
|
|
|
std::accumulate(forward_weights.begin(),
|
|
|
|
forward_weights.begin() + data.fwd_segment_position,
|
|
|
|
EdgeWeight{0});
|
2016-01-29 20:52:20 -05:00
|
|
|
|
2018-03-19 14:41:02 -04:00
|
|
|
const auto forward_duration_offset =
|
|
|
|
std::accumulate(forward_durations.begin(),
|
|
|
|
forward_durations.begin() + data.fwd_segment_position,
|
|
|
|
EdgeDuration{0});
|
|
|
|
|
|
|
|
EdgeWeight forward_weight = forward_weights[data.fwd_segment_position];
|
|
|
|
EdgeDuration forward_duration = forward_durations[data.fwd_segment_position];
|
|
|
|
|
|
|
|
BOOST_ASSERT(data.fwd_segment_position <
|
|
|
|
std::distance(forward_durations.begin(), forward_durations.end()));
|
|
|
|
|
|
|
|
const auto reverse_weight_offset =
|
|
|
|
std::accumulate(reverse_weights.begin(),
|
|
|
|
reverse_weights.end() - data.fwd_segment_position - 1,
|
|
|
|
EdgeWeight{0});
|
|
|
|
|
|
|
|
const auto reverse_duration_offset =
|
|
|
|
std::accumulate(reverse_durations.begin(),
|
|
|
|
reverse_durations.end() - data.fwd_segment_position - 1,
|
|
|
|
EdgeDuration{0});
|
|
|
|
|
|
|
|
EdgeWeight reverse_weight =
|
|
|
|
reverse_weights[reverse_weights.size() - data.fwd_segment_position - 1];
|
|
|
|
EdgeDuration reverse_duration =
|
|
|
|
reverse_durations[reverse_durations.size() - data.fwd_segment_position - 1];
|
2016-01-29 20:52:20 -05:00
|
|
|
|
|
|
|
ratio = std::min(1.0, std::max(0.0, ratio));
|
2016-03-28 11:06:51 -04:00
|
|
|
if (data.forward_segment_id.id != SPECIAL_SEGMENTID)
|
2016-03-03 08:26:13 -05:00
|
|
|
{
|
2016-05-12 12:50:10 -04:00
|
|
|
forward_weight = static_cast<EdgeWeight>(forward_weight * ratio);
|
2017-04-10 15:09:14 -04:00
|
|
|
forward_duration = static_cast<EdgeDuration>(forward_duration * ratio);
|
2015-12-03 14:04:23 -05:00
|
|
|
}
|
2016-03-28 11:06:51 -04:00
|
|
|
if (data.reverse_segment_id.id != SPECIAL_SEGMENTID)
|
2016-03-03 08:26:13 -05:00
|
|
|
{
|
2016-05-12 12:50:10 -04:00
|
|
|
reverse_weight -= static_cast<EdgeWeight>(reverse_weight * ratio);
|
2017-04-10 15:09:14 -04:00
|
|
|
reverse_duration -= static_cast<EdgeDuration>(reverse_duration * ratio);
|
2016-01-29 20:52:20 -05:00
|
|
|
}
|
|
|
|
|
2017-05-05 18:02:53 -04:00
|
|
|
// check phantom node segments validity
|
|
|
|
auto areSegmentsValid = [](auto first, auto last) -> bool {
|
2017-05-10 19:04:09 -04:00
|
|
|
return std::find(first, last, INVALID_SEGMENT_WEIGHT) == last;
|
2017-05-05 18:02:53 -04:00
|
|
|
};
|
|
|
|
bool is_forward_valid_source =
|
2018-03-19 14:41:02 -04:00
|
|
|
areSegmentsValid(forward_weights.begin(), forward_weights.end());
|
|
|
|
bool is_forward_valid_target = areSegmentsValid(
|
|
|
|
forward_weights.begin(), forward_weights.begin() + data.fwd_segment_position + 1);
|
2017-05-05 18:02:53 -04:00
|
|
|
bool is_reverse_valid_source =
|
2018-03-19 14:41:02 -04:00
|
|
|
areSegmentsValid(reverse_weights.begin(), reverse_weights.end());
|
2017-05-05 18:02:53 -04:00
|
|
|
bool is_reverse_valid_target = areSegmentsValid(
|
2018-03-19 14:41:02 -04:00
|
|
|
reverse_weights.begin(), reverse_weights.end() - data.fwd_segment_position);
|
2017-05-05 18:02:53 -04:00
|
|
|
|
2017-08-28 12:03:51 -04:00
|
|
|
auto transformed = PhantomNodeWithDistance{
|
|
|
|
PhantomNode{data,
|
|
|
|
component_id,
|
|
|
|
forward_weight,
|
|
|
|
reverse_weight,
|
|
|
|
forward_weight_offset,
|
|
|
|
reverse_weight_offset,
|
|
|
|
forward_duration,
|
|
|
|
reverse_duration,
|
|
|
|
forward_duration_offset,
|
|
|
|
reverse_duration_offset,
|
|
|
|
is_forward_valid_source,
|
|
|
|
is_forward_valid_target,
|
|
|
|
is_reverse_valid_source,
|
|
|
|
is_reverse_valid_target,
|
|
|
|
point_on_segment,
|
|
|
|
input_coordinate,
|
|
|
|
static_cast<unsigned short>(util::coordinate_calculation::bearing(
|
|
|
|
coordinates[data.u], coordinates[data.v]))},
|
|
|
|
current_perpendicular_distance};
|
2016-01-29 20:52:20 -05:00
|
|
|
|
2015-12-03 14:04:23 -05:00
|
|
|
return transformed;
|
|
|
|
}
|
|
|
|
|
2016-04-28 18:14:44 -04:00
|
|
|
bool CheckSegmentDistance(const Coordinate input_coordinate,
|
2016-03-28 14:38:19 -04:00
|
|
|
const CandidateSegment &segment,
|
2016-05-06 20:56:08 -04:00
|
|
|
const double max_distance) const
|
2016-03-28 14:38:19 -04:00
|
|
|
{
|
|
|
|
BOOST_ASSERT(segment.data.forward_segment_id.id != SPECIAL_SEGMENTID ||
|
|
|
|
!segment.data.forward_segment_id.enabled);
|
|
|
|
BOOST_ASSERT(segment.data.reverse_segment_id.id != SPECIAL_SEGMENTID ||
|
|
|
|
!segment.data.reverse_segment_id.enabled);
|
|
|
|
|
2016-04-08 20:18:47 -04:00
|
|
|
Coordinate wsg84_coordinate =
|
|
|
|
util::web_mercator::toWGS84(segment.fixed_projected_coordinate);
|
2016-03-28 14:38:19 -04:00
|
|
|
|
2016-04-08 20:18:47 -04:00
|
|
|
return util::coordinate_calculation::haversineDistance(input_coordinate, wsg84_coordinate) >
|
|
|
|
max_distance;
|
2016-03-28 14:38:19 -04:00
|
|
|
}
|
|
|
|
|
2017-08-16 16:21:19 -04:00
|
|
|
std::pair<bool, bool> CheckSegmentExclude(const CandidateSegment &segment) const
|
2017-07-28 09:59:48 -04:00
|
|
|
{
|
|
|
|
std::pair<bool, bool> valid = {true, true};
|
|
|
|
|
|
|
|
if (segment.data.forward_segment_id.enabled &&
|
2017-08-16 16:21:19 -04:00
|
|
|
datafacade.ExcludeNode(segment.data.forward_segment_id.id))
|
2017-07-28 09:59:48 -04:00
|
|
|
{
|
|
|
|
valid.first = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (segment.data.reverse_segment_id.enabled &&
|
2017-08-16 16:21:19 -04:00
|
|
|
datafacade.ExcludeNode(segment.data.reverse_segment_id.id))
|
2017-07-28 09:59:48 -04:00
|
|
|
{
|
|
|
|
valid.second = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return valid;
|
|
|
|
}
|
|
|
|
|
2016-04-28 18:14:44 -04:00
|
|
|
std::pair<bool, bool> CheckSegmentBearing(const CandidateSegment &segment,
|
2015-12-26 14:12:10 -05:00
|
|
|
const int filter_bearing,
|
2016-05-06 20:56:08 -04:00
|
|
|
const int filter_bearing_range) const
|
2015-12-03 14:04:23 -05:00
|
|
|
{
|
2016-03-28 14:38:19 -04:00
|
|
|
BOOST_ASSERT(segment.data.forward_segment_id.id != SPECIAL_SEGMENTID ||
|
|
|
|
!segment.data.forward_segment_id.enabled);
|
|
|
|
BOOST_ASSERT(segment.data.reverse_segment_id.id != SPECIAL_SEGMENTID ||
|
|
|
|
!segment.data.reverse_segment_id.enabled);
|
2016-03-28 11:06:51 -04:00
|
|
|
|
2016-01-07 19:31:57 -05:00
|
|
|
const double forward_edge_bearing = util::coordinate_calculation::bearing(
|
2016-05-06 20:43:37 -04:00
|
|
|
coordinates[segment.data.u], coordinates[segment.data.v]);
|
2015-12-03 14:04:23 -05:00
|
|
|
|
2015-12-26 14:12:10 -05:00
|
|
|
const double backward_edge_bearing = (forward_edge_bearing + 180) > 360
|
2016-01-05 06:04:04 -05:00
|
|
|
? (forward_edge_bearing - 180)
|
|
|
|
: (forward_edge_bearing + 180);
|
2015-12-03 14:04:23 -05:00
|
|
|
|
|
|
|
const bool forward_bearing_valid =
|
2016-05-27 15:05:04 -04:00
|
|
|
util::bearing::CheckInBounds(
|
|
|
|
std::round(forward_edge_bearing), filter_bearing, filter_bearing_range) &&
|
2016-03-28 14:38:19 -04:00
|
|
|
segment.data.forward_segment_id.enabled;
|
2015-12-03 14:04:23 -05:00
|
|
|
const bool backward_bearing_valid =
|
2016-05-27 15:05:04 -04:00
|
|
|
util::bearing::CheckInBounds(
|
|
|
|
std::round(backward_edge_bearing), filter_bearing, filter_bearing_range) &&
|
2016-03-28 14:38:19 -04:00
|
|
|
segment.data.reverse_segment_id.enabled;
|
2015-12-03 14:04:23 -05:00
|
|
|
return std::make_pair(forward_bearing_valid, backward_bearing_valid);
|
|
|
|
}
|
|
|
|
|
2016-07-28 17:09:55 -04:00
|
|
|
/**
|
|
|
|
* Checks to see if the edge weights are valid. We might have an edge,
|
2017-05-10 19:04:09 -04:00
|
|
|
* but a traffic update might set the speed to 0 (weight == INVALID_SEGMENT_WEIGHT).
|
2016-07-28 17:09:55 -04:00
|
|
|
* which means that this edge is not currently traversible. If this is the case,
|
|
|
|
* then we shouldn't snap to this edge.
|
|
|
|
*/
|
|
|
|
std::pair<bool, bool> HasValidEdge(const CandidateSegment &segment) const
|
|
|
|
{
|
|
|
|
|
|
|
|
bool forward_edge_valid = false;
|
|
|
|
bool reverse_edge_valid = false;
|
|
|
|
|
2017-05-11 03:13:59 -04:00
|
|
|
const auto &data = segment.data;
|
|
|
|
BOOST_ASSERT(data.forward_segment_id.enabled);
|
|
|
|
BOOST_ASSERT(data.forward_segment_id.id != SPECIAL_NODEID);
|
|
|
|
const auto geometry_id = datafacade.GetGeometryIndex(data.forward_segment_id.id).id;
|
|
|
|
|
2018-03-19 14:41:02 -04:00
|
|
|
const auto forward_weights = datafacade.GetUncompressedForwardWeights(geometry_id);
|
|
|
|
if (forward_weights[data.fwd_segment_position] != INVALID_SEGMENT_WEIGHT)
|
2016-07-28 17:09:55 -04:00
|
|
|
{
|
2017-05-11 03:13:59 -04:00
|
|
|
forward_edge_valid = data.forward_segment_id.enabled;
|
2016-07-28 17:09:55 -04:00
|
|
|
}
|
|
|
|
|
2018-03-19 14:41:02 -04:00
|
|
|
const auto reverse_weights = datafacade.GetUncompressedReverseWeights(geometry_id);
|
|
|
|
if (reverse_weights[reverse_weights.size() - data.fwd_segment_position - 1] !=
|
2017-05-10 19:04:09 -04:00
|
|
|
INVALID_SEGMENT_WEIGHT)
|
2016-07-28 17:09:55 -04:00
|
|
|
{
|
2017-05-11 03:13:59 -04:00
|
|
|
reverse_edge_valid = data.reverse_segment_id.enabled;
|
2016-07-28 17:09:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return std::make_pair(forward_edge_valid, reverse_edge_valid);
|
|
|
|
}
|
|
|
|
|
2017-05-15 06:15:00 -04:00
|
|
|
bool IsTinyComponent(const CandidateSegment &segment) const
|
|
|
|
{
|
|
|
|
const auto &data = segment.data;
|
|
|
|
BOOST_ASSERT(data.forward_segment_id.enabled);
|
|
|
|
BOOST_ASSERT(data.forward_segment_id.id != SPECIAL_NODEID);
|
|
|
|
return datafacade.GetComponentID(data.forward_segment_id.id).is_tiny;
|
|
|
|
}
|
|
|
|
|
2017-05-29 10:13:15 -04:00
|
|
|
std::pair<bool, bool> CheckApproach(const util::Coordinate &input_coordinate,
|
|
|
|
const CandidateSegment &segment,
|
|
|
|
const Approach approach) const
|
2017-05-23 06:23:22 -04:00
|
|
|
{
|
2017-05-29 08:46:11 -04:00
|
|
|
bool isOnewaySegment =
|
|
|
|
!(segment.data.forward_segment_id.enabled && segment.data.reverse_segment_id.enabled);
|
2017-05-29 10:13:15 -04:00
|
|
|
if (!isOnewaySegment && approach == Approach::CURB)
|
2017-05-23 06:23:22 -04:00
|
|
|
{
|
|
|
|
// Check the counter clockwise
|
|
|
|
//
|
|
|
|
// input_coordinate
|
|
|
|
// |
|
|
|
|
// |
|
|
|
|
// segment.data.u ---------------- segment.data.v
|
|
|
|
|
|
|
|
bool input_coordinate_is_at_right = !util::coordinate_calculation::isCCW(
|
|
|
|
coordinates[segment.data.u], coordinates[segment.data.v], input_coordinate);
|
|
|
|
|
2017-08-16 06:07:46 -04:00
|
|
|
if (datafacade.IsLeftHandDriving(segment.data.forward_segment_id.id))
|
2017-05-23 06:23:22 -04:00
|
|
|
input_coordinate_is_at_right = !input_coordinate_is_at_right;
|
|
|
|
|
|
|
|
return std::make_pair(input_coordinate_is_at_right, (!input_coordinate_is_at_right));
|
|
|
|
}
|
|
|
|
return std::make_pair(true, true);
|
|
|
|
}
|
|
|
|
|
2016-05-06 20:56:08 -04:00
|
|
|
const RTreeT &rtree;
|
2016-05-06 20:43:37 -04:00
|
|
|
const CoordinateList &coordinates;
|
2016-01-29 20:52:20 -05:00
|
|
|
DataFacadeT &datafacade;
|
2015-12-03 14:04:23 -05:00
|
|
|
};
|
2016-01-05 10:51:13 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-03 14:04:23 -05:00
|
|
|
#endif
|