Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:58:55 +01:00
parent 4af19165ec
commit 68073add76
12458 changed files with 12350765 additions and 2 deletions

11
tools/CMakeLists.txt Normal file
View file

@ -0,0 +1,11 @@
add_subdirectory(openlr)
add_subdirectory(poly_borders)
add_subdirectory(track_analyzing)
omim_add_tool_subdirectory(feature_list)
omim_add_tool_subdirectory(topography_generator)
omim_add_tool_subdirectory(track_generator)
if (NOT SKIP_QT_GUI)
omim_add_tool_subdirectory(skin_generator)
endif()

View file

@ -0,0 +1,50 @@
#!/usr/bin/env bash
#
# Downloads metadata from Google Play
#
set -euo pipefail
cd "$(dirname "$0")/../../android"
check_keys() {
if [[ ! -r "google-play.json" ]]; then
echo >&2 "Missing $PWD/google-play.json"
exit 2
fi
}
check_screenshots() {
if [[ ! -r "../screenshots/android/en-US/graphics/phone-screenshots/1.jpg" ]]; then
echo >&2 "Please checkout screenshots to $PWD/../screenshots"
exit 3
fi
}
download_metadata() {
set -x
./gradlew bootstrapGoogleRelease
for locale in src/google/play/listings/??-??; do
# Fix wrong file extension (.png instead of .jpg)
for png in $locale/graphics/*-screenshots/*.png; do
if [[ $(file -b "$png") =~ ^'JPEG ' ]]; then
echo "Fixing $png extension"
mv -f "$png" "${png%.png}.jpg"
fi
done
# Copy new files to screenshots repository
cp -Rpv $locale/graphics ../screenshots/android/$(basename $locale)/
# Replace original directory with a symlink to screenshots repository
rm -rf $locale/graphics
ln -sf ../../../../../../screenshots/android/$(basename $locale)/graphics $locale/graphics
done
# Ignore changelogs from all tracks exception production
mv -f src/google/play/release-notes/en-US/production.txt src/google/play/release-notes/en-US/default.txt
rm -f src/google/play/release-notes/en-US/alpha.txt src/google/play/release-notes/en-US/beta.txt src/google/play/release-notes/en-US/internal.txt
}
check_keys
check_screenshots
download_metadata

View file

@ -0,0 +1,41 @@
#!/usr/bin/env python3
import requests
import zipfile
import os
import shutil
def download_vulkan_validation_layers():
# Step 1: Download zip archive
url = "https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases/download/vulkan-sdk-1.3.275.0/android-binaries-1.3.275.0.zip"
download_path = "vulkan_sdk.zip"
print("Downloading zip archive...")
response = requests.get(url)
with open(download_path, "wb") as f:
f.write(response.content)
print("Download complete.")
# Step 2: Unzip archive
unzip_dir = "vulkan_sdk"
print("Unzipping archive...")
with zipfile.ZipFile(download_path, "r") as zip_ref:
zip_ref.extractall(unzip_dir)
print("Unzip complete.")
# Step 3: Rename unpacked folder to "jniLibs" and move it to "android/app/src/main"
unpacked_folder = os.listdir(unzip_dir)[0]
jniLibs_path = os.path.join(unzip_dir, "jniLibs")
os.rename(os.path.join(unzip_dir, unpacked_folder), jniLibs_path)
destination_path = "android/app/src/main"
shutil.move(jniLibs_path, destination_path)
print("Deploy complete.")
# Clean up downloaded zip file and empty directories
os.remove(download_path)
os.rmdir(unzip_dir)
print("Clean up complete.")
if __name__ == '__main__':
download_vulkan_validation_layers()

96
tools/android/set_up_android.py Executable file
View file

@ -0,0 +1,96 @@
#!/usr/bin/env python3
import os, sys, shutil, collections
from optparse import OptionParser
# Fix for python 2
try:
input = raw_input
except NameError:
pass
def find_recursive(root, subpath, maxdepth=4):
queue = collections.deque([(root, 0)])
if 'PATH' in os.environ:
envpath = os.environ['PATH'].split(':')
relpath = ['..'] * (len(subpath) - 1)
queue.extendleft([(os.path.join(x, *relpath), maxdepth) for x in envpath if 'android' in x.lower()])
while len(queue) > 0:
item = queue.popleft()
if os.path.isfile(os.path.join(item[0], *subpath)):
return os.path.abspath(item[0])
if item[1] < maxdepth:
for name in os.listdir(item[0]):
fullname = os.path.join(item[0], name)
if os.path.isdir(fullname) and '.' not in name:
queue.append((fullname, item[1] + 1))
return None
def read_local_properties():
androidRoot = os.path.join(os.path.dirname(sys.argv[0]), '..', '..', 'android')
propsFile = os.path.join(androidRoot, 'local.properties')
sdkDir = None
if os.path.exists(propsFile):
with open(propsFile, 'r') as f:
for line in f:
line = line.strip()
if line.startswith('sdk.dir') and '=' in line:
sdkDir = line.split('=')[1].strip()
return sdkDir
def test_path(title, path, subpath):
test = os.path.join(path, *subpath)
if path and os.path.isfile(test):
return os.path.abspath(path)
print('Could not find "{0}", it\'s not an {1} path!'.format(test, title))
return None
def query_path(title, option, default, subpath):
if option:
path = test_path(title, option, subpath)
if path:
return path
default = '' if not default else os.path.abspath(default)
searchHint = ', "s" to search'
while True:
path = input('Path to {0}{1} [{2}]:'.format(title, searchHint, default)) or default
if len(searchHint) > 0 and path.lower().strip() == 's':
found = find_recursive(os.path.expanduser('~'), subpath)
if found:
print('Found {0} at "{1}".'.format(title, found))
default = found
else:
print('{0} not found.'.format(title))
searchHint = ''
else:
break
return test_path(title, path, subpath)
def write_local_properties(sdkDir):
sdkProp = 'sdk.dir={0}'.format(sdkDir)
content = ''.join([x + '\n' for x in [
'# Autogenerated file',
'# Do not add it to version control',
sdkProp,
]])
# Create android/local.properties
androidRoot = os.path.join(os.path.dirname(sys.argv[0]), '..', '..', 'android')
propsFile = os.path.abspath(os.path.join(androidRoot, 'local.properties'))
print('Writing "{0}" to {1}'.format(sdkProp, propsFile))
with open(propsFile, 'w') as f:
f.write(content)
if __name__ == '__main__':
parser = OptionParser()
parser.set_description(' '.join([
'Writes Android SDK location into android/local.properties file.',
'Prompts for SDK location and offers to search for it if started without options.',
]))
parser.add_option('-s', '--sdk', help='path to Android SDK')
options, _ = parser.parse_args()
sdkDirOld = read_local_properties()
sdkDir = query_path('Android SDK', options.sdk, sdkDirOld, ['platform-tools', 'adb'])
if sdkDir:
write_local_properties(sdkDir)

16
tools/autobuild/detect_cmake.sh Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -e -u
# If CMAKE variable is set, use it
[ -n "${CMAKE-}" -a -x "${CMAKE-}" ] && return 0
# Find cmake, prefer cmake3
for name in cmake3 cmake; do
if command -v "$name" > /dev/null; then
CMAKE="$name"
return 0
fi
done
echo 'Error: cmake is not installed.' >&2
exit 1

View file

@ -0,0 +1,46 @@
set -e -u
MY_PATH="`dirname \"$0\"`" # relative
MY_PATH="`( cd \"$MY_PATH\" && pwd )`" # absolutized and normalized
# Echoes found NDK root path or nothing if not found
# return 1 on error and 0 on success
GetNdkRoot()
{
local FILENAME="$MY_PATH/../../android/local.properties"
while read line
do
if [[ "${line:0:7}" == "ndk.dir" ]]; then
echo "${line:8}"
return 0
fi
done < $FILENAME
# Not found in local.properties, try to find using ANDROID_HOME.
local NDK_VERSION="`( ls -1 "${ANDROID_HOME}/ndk" | sort | tail -1 )`"
if [ ! -z "$NDK_VERSION" ]
then
echo "$ANDROID_HOME/ndk/$NDK_VERSION"
return 0
fi
return 1
}
GetNdkHost()
{
if [[ "${OSTYPE:0:5}" == "linux" ]]; then
echo "linux-x86_64"
return 0
fi
if [[ "${OSTYPE:0:6}" == "darwin" ]]; then
echo "darwin-x86_64"
return 0
fi
if [[ "$OSTYPE" == "cygwin" ]]; then
echo windows-x86_64
return 0
fi
echo "ERROR: Can't detect your host OS"
return 1
}

View file

@ -0,0 +1,13 @@
project(feature_list_generator)
set(SRC feature_list.cpp)
omim_add_executable(${PROJECT_NAME} ${SRC})
target_link_libraries(${PROJECT_NAME}
mwm_diff
generator
search
platform
indexer
)

View file

@ -0,0 +1,436 @@
#include "generator/utils.hpp"
#include "search/search_quality/helpers.hpp"
#include "search/categories_cache.hpp"
#include "search/engine.hpp"
#include "search/locality_finder.hpp"
#include "search/reverse_geocoder.hpp"
#include "storage/country_info_getter.hpp"
#include "storage/storage.hpp"
#include "storage/storage_defines.hpp"
#include "indexer/classificator.hpp"
#include "indexer/classificator_loader.hpp"
#include "indexer/data_source.hpp"
#include "indexer/feature.hpp"
#include "indexer/ftypes_matcher.hpp"
#include "indexer/map_object.hpp"
#include "indexer/map_style_reader.hpp"
#include "platform/platform_tests_support/helpers.hpp"
#include "platform/local_country_file_utils.hpp"
#include "platform/platform.hpp"
#include "geometry/mercator.hpp"
#include "geometry/point2d.hpp"
#include "base/file_name_utils.hpp"
#include "base/logging.hpp"
#include "base/string_utils.hpp"
#include <algorithm>
#include <iostream>
#include <limits>
#include <map>
#include <sstream>
#include <vector>
using namespace std;
class ClosestPoint
{
m2::PointD const & m_center;
m2::PointD m_best;
double m_distance = numeric_limits<double>::max();
public:
explicit ClosestPoint(m2::PointD const & center) : m_center(center), m_best(0, 0) {}
m2::PointD GetBest() const { return m_best; }
void operator()(m2::PointD const & point)
{
double distance = m_center.SquaredLength(point);
if (distance < m_distance)
{
m_distance = distance;
m_best = point;
}
}
};
m2::PointD FindCenter(FeatureType & f)
{
ClosestPoint closest(f.GetLimitRect(FeatureType::BEST_GEOMETRY).Center());
if (f.GetGeomType() == feature::GeomType::Area)
{
f.ForEachTriangle([&closest](m2::PointD const & p1, m2::PointD const & p2, m2::PointD const & p3)
{ closest((p1 + p2 + p3) / 3); }, FeatureType::BEST_GEOMETRY);
}
else
{
f.ForEachPoint(closest, FeatureType::BEST_GEOMETRY);
}
return closest.GetBest();
}
size_t const kLangCount = StringUtf8Multilang::GetSupportedLanguages().size();
string GetReadableType(FeatureType & f)
{
string result;
f.ForEachType([&](uint32_t type)
{
if (ftypes::IsPoiChecker::Instance()(type) || ftypes::IsPlaceChecker::Instance()(type))
result = classif().GetReadableObjectName(type);
});
return result;
}
string GetWheelchairType(FeatureType & f)
{
static uint32_t const wheelchair = classif().GetTypeByPath({"wheelchair"});
string result;
f.ForEachType([&result](uint32_t type)
{
if (ftype::Trunc(type, 1) == wheelchair)
{
string fullName = classif().GetReadableObjectName(type);
auto pos = fullName.find("-");
if (pos != string::npos)
result = fullName.substr(pos + 1);
}
});
return result;
}
bool HasAtm(FeatureType & f)
{
static uint32_t const atm = classif().GetTypeByPath({"amenity", "atm"});
bool result = false;
f.ForEachType([&result](uint32_t type)
{
if (type == atm)
result = true;
});
return result;
}
string BuildUniqueId(ms::LatLon const & coords, string const & name)
{
ostringstream ss;
ss << strings::to_string_dac(coords.m_lat, 6) << ',' << strings::to_string_dac(coords.m_lon, 6) << ',' << name;
uint32_t hash = 0;
for (char const c : ss.str())
hash = hash * 101 + c;
return strings::to_string(hash);
}
void AppendNames(FeatureType & f, vector<string> & columns)
{
vector<string> names(kLangCount);
f.GetNames().ForEach([&names](int8_t code, string_view name) { names[code] = name; });
columns.insert(columns.end(), next(names.begin()), names.end());
}
void PrintAsCSV(vector<string> const & columns, char const delimiter, ostream & out)
{
bool first = true;
for (string value : columns)
{
// Newlines are hard to process, replace them with spaces. And trim the
// string.
replace(value.begin(), value.end(), '\r', ' ');
replace(value.begin(), value.end(), '\n', ' ');
strings::Trim(value);
if (first)
first = false;
else
out << delimiter;
bool needsQuotes = value.find('"') != string::npos || value.find(delimiter) != string::npos;
if (!needsQuotes)
{
out << value;
}
else
{
size_t pos = 0;
while ((pos = value.find('"', pos)) != string::npos)
{
value.insert(pos, 1, '"');
pos += 2;
}
out << '"' << value << '"';
}
}
out << endl;
}
class Processor
{
search::ReverseGeocoder m_geocoder;
base::Cancellable m_cancellable;
search::CitiesBoundariesTable m_boundariesTable;
search::VillagesCache m_villagesCache;
search::LocalityFinder m_finder;
public:
explicit Processor(DataSource const & dataSource)
: m_geocoder(dataSource)
, m_boundariesTable(dataSource)
, m_villagesCache(m_cancellable)
, m_finder(dataSource, m_boundariesTable, m_villagesCache)
{
m_boundariesTable.Load();
}
void ClearCache() { m_villagesCache.Clear(); }
void operator()(FeatureType & f, map<uint32_t, base::GeoObjectId> const & ft2osm) { Process(f, ft2osm); }
void Process(FeatureType & f, map<uint32_t, base::GeoObjectId> const & ft2osm)
{
f.ParseAllBeforeGeometry();
string const & category = GetReadableType(f);
auto const & meta = f.GetMetadata();
auto const metaOperator = meta.Get(feature::Metadata::FMD_OPERATOR);
auto const & osmIt = ft2osm.find(f.GetID().m_index);
if ((!f.HasName() && metaOperator.empty()) ||
(f.GetGeomType() == feature::GeomType::Line && category != "highway-pedestrian") || category.empty())
{
return;
}
m2::PointD const & center = FindCenter(f);
ms::LatLon const & ll = mercator::ToLatLon(center);
osm::MapObject obj;
obj.SetFromFeatureType(f);
string_view city;
m_finder.GetLocality(center, [&city](search::LocalityItem const & item)
{ item.GetSpecifiedOrDefaultName(StringUtf8Multilang::kDefaultCode, city); });
string const & mwmName = f.GetID().GetMwmName();
string name(f.GetName(StringUtf8Multilang::kDefaultCode));
if (name.empty())
{
name = f.GetReadableName();
if (name.empty())
name = metaOperator;
}
string osmId = osmIt != ft2osm.cend() ? to_string(osmIt->second.GetEncodedId()) : "";
string const & uid = BuildUniqueId(ll, name);
string const & lat = strings::to_string_dac(ll.m_lat, 6);
string const & lon = strings::to_string_dac(ll.m_lon, 6);
search::ReverseGeocoder::Address addr;
string addrStreet = "";
string addrHouse = "";
double constexpr kDistanceThresholdMeters = 0.5;
if (m_geocoder.GetExactAddress(f, addr))
{
addrStreet = addr.GetStreetName();
addrHouse = addr.GetHouseNumber();
}
else
{
m_geocoder.GetNearbyAddress(center, addr);
if (addr.GetDistance() < kDistanceThresholdMeters)
{
addrStreet = addr.GetStreetName();
addrHouse = addr.GetHouseNumber();
}
}
string const phone(meta.Get(feature::Metadata::FMD_PHONE_NUMBER));
string const website(meta.Get(feature::Metadata::FMD_WEBSITE));
string const contact_facebook(meta.Get(feature::Metadata::FMD_CONTACT_FACEBOOK));
string const contact_instagram(meta.Get(feature::Metadata::FMD_CONTACT_INSTAGRAM));
string const contact_twitter(meta.Get(feature::Metadata::FMD_CONTACT_TWITTER));
string const contact_vk(meta.Get(feature::Metadata::FMD_CONTACT_VK));
string const contact_line(meta.Get(feature::Metadata::FMD_CONTACT_LINE));
string const contact_fediverse(meta.Get(feature::Metadata::FMD_CONTACT_FEDIVERSE));
string const contact_bluesky(meta.Get(feature::Metadata::FMD_CONTACT_BLUESKY));
string const stars(meta.Get(feature::Metadata::FMD_STARS));
string const branch(meta.Get(feature::Metadata::FMD_BRANCH));
string const internet(meta.Get(feature::Metadata::FMD_INTERNET));
string const denomination(meta.Get(feature::Metadata::FMD_DENOMINATION));
string const wheelchair(GetWheelchairType(f));
string const opening_hours(meta.Get(feature::Metadata::FMD_OPEN_HOURS));
string const check_date(meta.Get(feature::Metadata::FMD_CHECK_DATE));
string const check_date_opening_hours(meta.Get(feature::Metadata::FMD_CHECK_DATE_OPEN_HOURS));
string const wikipedia(meta.Get(feature::Metadata::FMD_WIKIPEDIA));
string const wikimedia_commons(meta.Get(feature::Metadata::FMD_WIKIMEDIA_COMMONS));
string const panoramax(meta.Get(feature::Metadata::FMD_PANORAMAX));
string const floor(meta.Get(feature::Metadata::FMD_LEVEL));
string const fee = category.ends_with("-fee") ? "yes" : "";
string const atm = HasAtm(f) ? "yes" : "";
vector<string> columns = {osmId,
uid,
lat,
lon,
mwmName,
category,
name,
std::string(city),
addrStreet,
addrHouse,
phone,
website,
stars,
std::string(metaOperator),
branch,
internet,
denomination,
wheelchair,
opening_hours,
check_date,
check_date_opening_hours,
wikipedia,
floor,
fee,
atm,
contact_facebook,
contact_instagram,
contact_twitter,
contact_vk,
contact_line,
contact_fediverse,
contact_bluesky,
wikimedia_commons,
panoramax};
AppendNames(f, columns);
PrintAsCSV(columns, ';', cout);
}
};
void PrintHeader()
{
vector<string> columns = {"id",
"old_id",
"lat",
"lon",
"mwm",
"category",
"name",
"city",
"street",
"house",
"phone",
"website",
"cuisines",
"stars",
"operator",
"branch",
"internet",
"denomination",
"wheelchair",
"opening_hours",
"check_date",
"check_date_opening_hours",
"wikipedia",
"floor",
"fee",
"atm",
"contact_facebook",
"contact_instagram",
"contact_twitter",
"contact_vk",
"contact_line",
"contact_fediverse",
"contact_bluesky",
"wikimedia_commons",
"panoramax"};
// Append all supported name languages in order.
for (uint8_t idx = 1; idx < kLangCount; idx++)
columns.push_back("name_" + string(StringUtf8Multilang::GetLangByCode(idx)));
PrintAsCSV(columns, ';', cout);
}
bool ParseFeatureIdToOsmIdMapping(string const & path, map<uint32_t, base::GeoObjectId> & mapping)
{
return generator::ForEachOsmId2FeatureId(
path, [&](auto const & compositeId, uint32_t const featureId) { mapping[featureId] = compositeId.m_mainId; });
}
void DidDownload(storage::CountryId const & /* countryId */,
shared_ptr<platform::LocalCountryFile> const & /* localFile */)
{}
bool WillDelete(storage::CountryId const & /* countryId */,
shared_ptr<platform::LocalCountryFile> const & /* localFile */)
{
return false;
}
int main(int argc, char ** argv)
{
platform::tests_support::ChangeMaxNumberOfOpenFiles(search::search_quality::kMaxOpenFiles);
if (argc <= 1)
{
LOG(LERROR, ("Usage:", argc == 1 ? argv[0] : "feature_list", "<mwm_path> [<data_path>] [<mwm_prefix>]"));
return 1;
}
Platform & pl = GetPlatform();
pl.SetWritableDirForTests(argv[1]);
string countriesFile = COUNTRIES_FILE;
if (argc > 2)
{
pl.SetResourceDir(argv[2]);
countriesFile = base::JoinPath(argv[2], COUNTRIES_FILE);
}
storage::Storage storage(countriesFile, argv[1]);
storage.Init(&DidDownload, &WillDelete);
auto infoGetter = storage::CountryInfoReader::CreateCountryInfoGetter(pl);
infoGetter->SetAffiliations(storage.GetAffiliations());
GetStyleReader().SetCurrentStyle(MapStyleMerged);
classificator::Load();
FrozenDataSource dataSource;
vector<platform::LocalCountryFile> mwms;
platform::FindAllLocalMapsAndCleanup(numeric_limits<int64_t>::max() /* the latest version */, mwms);
for (auto & mwm : mwms)
{
mwm.SyncWithDisk();
auto const & p = dataSource.RegisterMap(mwm);
CHECK_EQUAL(MwmSet::RegResult::Success, p.second, ("Could not register map", mwm));
MwmSet::MwmId const & id = p.first;
CHECK(id.IsAlive(), ("Mwm is not alive?", mwm));
}
Processor doProcess(dataSource);
PrintHeader();
vector<shared_ptr<MwmInfo>> mwmInfos;
dataSource.GetMwmsInfo(mwmInfos);
for (auto const & mwmInfo : mwmInfos)
{
if (mwmInfo->GetType() != MwmInfo::COUNTRY)
continue;
if (argc > 3 && !(mwmInfo->GetCountryName() + DATA_FILE_EXTENSION).starts_with(argv[3]))
continue;
LOG(LINFO, ("Processing", mwmInfo->GetCountryName()));
string osmToFeatureFile =
base::JoinPath(argv[1], mwmInfo->GetCountryName() + DATA_FILE_EXTENSION + OSM2FEATURE_FILE_EXTENSION);
map<uint32_t, base::GeoObjectId> featureIdToOsmId;
ParseFeatureIdToOsmIdMapping(osmToFeatureFile, featureIdToOsmId);
MwmSet::MwmId mwmId(mwmInfo);
FeaturesLoaderGuard loader(dataSource, mwmId);
for (uint32_t ftIndex = 0; ftIndex < loader.GetNumFeatures(); ftIndex++)
if (auto ft = loader.GetFeatureByIndex(static_cast<uint32_t>(ftIndex)))
doProcess.Process(*ft, featureIdToOsmId);
doProcess.ClearCache();
}
return 0;
}

View file

@ -0,0 +1,50 @@
project(openlr)
set(SRC
cache_line_size.hpp
candidate_paths_getter.cpp
candidate_paths_getter.hpp
candidate_points_getter.cpp
candidate_points_getter.hpp
decoded_path.cpp
decoded_path.hpp
graph.cpp
graph.hpp
helpers.cpp
helpers.hpp
openlr_decoder.cpp
openlr_decoder.hpp
openlr_model.cpp
openlr_model.hpp
openlr_model_xml.cpp
openlr_model_xml.hpp
paths_connector.cpp
paths_connector.hpp
road_info_getter.cpp
road_info_getter.hpp
router.cpp
router.hpp
score_candidate_paths_getter.cpp
score_candidate_paths_getter.hpp
score_candidate_points_getter.cpp
score_candidate_points_getter.hpp
score_paths_connector.cpp
score_paths_connector.hpp
score_types.hpp
stats.hpp
way_point.hpp
)
omim_add_library(${PROJECT_NAME} ${SRC})
target_link_libraries(${PROJECT_NAME}
routing
indexer
geometry
pugixml
)
omim_add_tool_subdirectory(openlr_match_quality)
omim_add_tool_subdirectory(openlr_stat)
omim_add_test_subdirectory(openlr_tests)

View file

@ -0,0 +1,8 @@
#pragma once
#include <cstddef>
namespace openlr
{
size_t constexpr kCacheLineSize = 64;
} // namespace openlr

View file

@ -0,0 +1,275 @@
#include "openlr/candidate_paths_getter.hpp"
#include "openlr/candidate_points_getter.hpp"
#include "openlr/graph.hpp"
#include "openlr/helpers.hpp"
#include "openlr/openlr_model.hpp"
#include "routing/road_graph.hpp"
#include "platform/location.hpp"
#include "geometry/angles.hpp"
#include "geometry/point_with_altitude.hpp"
#include <algorithm>
#include <iterator>
#include <queue>
#include <set>
#include <tuple>
using namespace std;
using namespace routing;
namespace openlr
{
namespace cpg
{
int const kNumBuckets = 256;
double const kAnglesInBucket = 360.0 / kNumBuckets;
uint32_t Bearing(m2::PointD const & a, m2::PointD const & b)
{
auto const angle = location::AngleToBearing(math::RadToDeg(ang::AngleTo(a, b)));
CHECK_LESS_OR_EQUAL(angle, 360, ("Angle should be less than or equal to 360."));
CHECK_GREATER_OR_EQUAL(angle, 0, ("Angle should be greater than or equal to 0"));
return math::Clamp(angle / kAnglesInBucket, 0.0, 255.0);
}
} // namespace cpg
// CandidatePathsGetter::Link ----------------------------------------------------------------------
Graph::Edge CandidatePathsGetter::Link::GetStartEdge() const
{
auto * start = this;
while (start->m_parent)
start = start->m_parent.get();
return start->m_edge;
}
bool CandidatePathsGetter::Link::IsPointOnPath(geometry::PointWithAltitude const & point) const
{
for (auto * l = this; l; l = l->m_parent.get())
{
if (l->m_edge.GetEndJunction() == point)
{
LOG(LDEBUG, ("A loop detected, skipping..."));
return true;
}
}
return false;
}
// CandidatePathsGetter ----------------------------------------------------------------------------
bool CandidatePathsGetter::GetLineCandidatesForPoints(vector<LocationReferencePoint> const & points,
vector<vector<Graph::EdgeVector>> & lineCandidates)
{
for (size_t i = 0; i < points.size(); ++i)
{
if (i != points.size() - 1 && points[i].m_distanceToNextPoint == 0)
{
LOG(LINFO, ("Distance to next point is zero. Skipping the whole segment"));
++m_stats.m_zeroDistToNextPointCount;
return false;
}
lineCandidates.emplace_back();
auto const isLastPoint = i == points.size() - 1;
double const distanceToNextPointM = (isLastPoint ? points[i - 1] : points[i]).m_distanceToNextPoint;
vector<m2::PointD> pointCandidates;
m_pointsGetter.GetCandidatePoints(mercator::FromLatLon(points[i].m_latLon), pointCandidates);
GetLineCandidates(points[i], isLastPoint, distanceToNextPointM, pointCandidates, lineCandidates.back());
if (lineCandidates.back().empty())
{
LOG(LINFO, ("No candidate lines found for point", points[i].m_latLon, "Giving up"));
++m_stats.m_noCandidateFound;
return false;
}
}
ASSERT_EQUAL(lineCandidates.size(), points.size(), ());
return true;
}
void CandidatePathsGetter::GetStartLines(vector<m2::PointD> const & points, bool const isLastPoint,
Graph::EdgeVector & edges)
{
for (auto const & pc : points)
{
Graph::EdgeListT tmp;
if (!isLastPoint)
m_graph.GetOutgoingEdges(geometry::PointWithAltitude(pc, 0 /* altitude */), tmp);
else
m_graph.GetIngoingEdges(geometry::PointWithAltitude(pc, 0 /* altitude */), tmp);
edges.insert(edges.end(), tmp.begin(), tmp.end());
}
// Same edges may start on different points if those points are close enough.
base::SortUnique(edges, less<Graph::Edge>(), EdgesAreAlmostEqual);
}
void CandidatePathsGetter::GetAllSuitablePaths(Graph::EdgeVector const & startLines, bool isLastPoint, double bearDistM,
FunctionalRoadClass functionalRoadClass, FormOfWay formOfWay,
double distanceToNextPointM, vector<LinkPtr> & allPaths)
{
queue<LinkPtr> q;
for (auto const & e : startLines)
{
auto const u = make_shared<Link>(nullptr /* parent */, e, 0 /* distanceM */);
q.push(u);
}
while (!q.empty())
{
auto const u = q.front();
q.pop();
auto const & currentEdge = u->m_edge;
auto const currentEdgeLen = EdgeLength(currentEdge);
// TODO(mgsergio): Maybe weak this constraint a bit.
if (u->m_distanceM + currentEdgeLen >= bearDistM)
{
allPaths.push_back(u);
continue;
}
ASSERT_LESS(u->m_distanceM + currentEdgeLen, bearDistM, ());
Graph::EdgeListT edges;
if (!isLastPoint)
m_graph.GetOutgoingEdges(currentEdge.GetEndJunction(), edges);
else
m_graph.GetIngoingEdges(currentEdge.GetStartJunction(), edges);
for (auto const & e : edges)
{
// Fake edges are allowed only at the start/end of the path.
if (e.IsFake())
continue;
if (EdgesAreAlmostEqual(e.GetReverseEdge(), currentEdge))
continue;
ASSERT(currentEdge.HasRealPart(), ());
if (!PassesRestriction(e, functionalRoadClass, formOfWay, 2 /* kFRCThreshold */, m_infoGetter))
continue;
// TODO(mgsergio): Should we check form of way as well?
if (u->IsPointOnPath(e.GetEndJunction()))
continue;
auto const p = make_shared<Link>(u, e, u->m_distanceM + currentEdgeLen);
q.push(p);
}
}
}
void CandidatePathsGetter::GetBestCandidatePaths(vector<LinkPtr> const & allPaths, bool const isLastPoint,
uint32_t const requiredBearing, double const bearDistM,
m2::PointD const & startPoint, vector<Graph::EdgeVector> & candidates)
{
set<CandidatePath> candidatePaths;
set<CandidatePath> fakeEndingsCandidatePaths;
BearingPointsSelector pointsSelector(bearDistM, isLastPoint);
for (auto const & l : allPaths)
{
auto const bearStartPoint = pointsSelector.GetStartPoint(l->GetStartEdge());
auto const startPointsDistance = mercator::DistanceOnEarth(bearStartPoint, startPoint);
// Number of edges counting from the last one to check bearing on. Accorfing to OpenLR spec
// we have to check bearing on a point that is no longer than 25 meters traveling down the path.
// But sometimes we may skip the best place to stop and generate a candidate. So we check several
// edges before the last one to avoid such a situation. Number of iterations is taken
// by intuition.
// Example:
// o -------- o { Partners segment. }
// o ------- o --- o { Our candidate. }
// ^ 25m
// ^ This one may be better than
// ^ this one.
// So we want to check them all.
uint32_t traceBackIterationsLeft = 3;
for (auto part = l; part; part = part->m_parent)
{
if (traceBackIterationsLeft == 0)
break;
--traceBackIterationsLeft;
auto const bearEndPoint = pointsSelector.GetEndPoint(part->m_edge, part->m_distanceM);
auto const bearing = cpg::Bearing(bearStartPoint, bearEndPoint);
auto const bearingDiff = AbsDifference(bearing, requiredBearing);
auto const pathDistDiff = AbsDifference(part->m_distanceM, bearDistM);
// TODO(mgsergio): Check bearing is within tolerance. Add to canidates if it is.
if (part->m_hasFake)
fakeEndingsCandidatePaths.emplace(part, bearingDiff, pathDistDiff, startPointsDistance);
else
candidatePaths.emplace(part, bearingDiff, pathDistDiff, startPointsDistance);
}
}
ASSERT(none_of(begin(candidatePaths), end(candidatePaths), mem_fn(&CandidatePath::HasFakeEndings)), ());
ASSERT(fakeEndingsCandidatePaths.empty() || any_of(begin(fakeEndingsCandidatePaths), end(fakeEndingsCandidatePaths),
mem_fn(&CandidatePath::HasFakeEndings)),
());
vector<CandidatePath> paths;
copy_n(begin(candidatePaths), min(static_cast<size_t>(kMaxCandidates), candidatePaths.size()), back_inserter(paths));
copy_n(begin(fakeEndingsCandidatePaths),
min(static_cast<size_t>(kMaxFakeCandidates), fakeEndingsCandidatePaths.size()), back_inserter(paths));
LOG(LDEBUG, ("List candidate paths..."));
for (auto const & path : paths)
{
LOG(LDEBUG, ("CP:", path.m_bearingDiff, path.m_pathDistanceDiff, path.m_startPointDistance));
Graph::EdgeVector edges;
for (auto * p = path.m_path.get(); p; p = p->m_parent.get())
edges.push_back(p->m_edge);
if (!isLastPoint)
reverse(begin(edges), end(edges));
candidates.emplace_back(std::move(edges));
}
}
void CandidatePathsGetter::GetLineCandidates(openlr::LocationReferencePoint const & p, bool const isLastPoint,
double const distanceToNextPointM,
vector<m2::PointD> const & pointCandidates,
vector<Graph::EdgeVector> & candidates)
{
double const kDefaultBearDistM = 25.0;
double const bearDistM = min(kDefaultBearDistM, distanceToNextPointM);
LOG(LINFO, ("BearDist is", bearDistM));
Graph::EdgeVector startLines;
GetStartLines(pointCandidates, isLastPoint, startLines);
LOG(LINFO, (startLines.size(), "start line candidates found for point (LatLon)", p.m_latLon));
LOG(LDEBUG, ("Listing start lines:"));
for (auto const & e : startLines)
LOG(LDEBUG, (LogAs2GisPath(e)));
auto const startPoint = mercator::FromLatLon(p.m_latLon);
vector<LinkPtr> allPaths;
GetAllSuitablePaths(startLines, isLastPoint, bearDistM, p.m_functionalRoadClass, p.m_formOfWay, distanceToNextPointM,
allPaths);
GetBestCandidatePaths(allPaths, isLastPoint, p.m_bearing, bearDistM, startPoint, candidates);
LOG(LDEBUG, (candidates.size(), "candidate paths found for point (LatLon)", p.m_latLon));
}
} // namespace openlr

View file

@ -0,0 +1,124 @@
#pragma once
#include "openlr/graph.hpp"
#include "openlr/openlr_model.hpp"
#include "openlr/road_info_getter.hpp"
#include "openlr/stats.hpp"
#include "geometry/point2d.hpp"
#include <cstddef>
#include <cstdint>
#include <limits>
#include <memory>
#include <vector>
namespace openlr
{
class CandidatePointsGetter;
class CandidatePathsGetter
{
public:
CandidatePathsGetter(CandidatePointsGetter & pointsGetter, Graph & graph, RoadInfoGetter & infoGetter,
v2::Stats & stat)
: m_pointsGetter(pointsGetter)
, m_graph(graph)
, m_infoGetter(infoGetter)
, m_stats(stat)
{}
bool GetLineCandidatesForPoints(std::vector<LocationReferencePoint> const & points,
std::vector<std::vector<Graph::EdgeVector>> & lineCandidates);
private:
struct Link;
using LinkPtr = std::shared_ptr<Link>;
size_t static constexpr kMaxCandidates = 5;
size_t static constexpr kMaxFakeCandidates = 2;
// TODO(mgsergio): Rename to Vertex.
struct Link
{
Link(LinkPtr const & parent, Graph::Edge const & edge, double const distanceM)
: m_parent(parent)
, m_edge(edge)
, m_distanceM(distanceM)
, m_hasFake((parent && parent->m_hasFake) || edge.IsFake())
{}
bool IsPointOnPath(geometry::PointWithAltitude const & point) const;
Graph::Edge GetStartEdge() const;
LinkPtr const m_parent;
Graph::Edge const m_edge;
double const m_distanceM;
bool const m_hasFake;
};
struct CandidatePath
{
double static constexpr kBearingDiffFactor = 5;
double static constexpr kPathDistanceFactor = 1;
double static constexpr kPointDistanceFactor = 2;
CandidatePath() = default;
CandidatePath(LinkPtr const path, uint32_t const bearingDiff, double const pathDistanceDiff,
double const startPointDistance)
: m_path(path)
, m_bearingDiff(bearingDiff)
, m_pathDistanceDiff(pathDistanceDiff)
, m_startPointDistance(startPointDistance)
{}
bool operator<(CandidatePath const & o) const { return GetPenalty() < o.GetPenalty(); }
double GetPenalty() const
{
return kBearingDiffFactor * m_bearingDiff + kPathDistanceFactor * m_pathDistanceDiff +
kPointDistanceFactor * m_startPointDistance;
}
bool HasFakeEndings() const { return m_path && m_path->m_hasFake; }
LinkPtr m_path = nullptr;
uint32_t m_bearingDiff = std::numeric_limits<uint32_t>::max(); // Domain is roughly [0, 30]
double m_pathDistanceDiff = std::numeric_limits<double>::max(); // Domain is roughly [0, 25]
double m_startPointDistance = std::numeric_limits<double>::max(); // Domain is roughly [0, 50]
};
// Note: In all methods below if |isLastPoint| is true than algorithm should
// calculate all parameters (such as bearing, distance to next point, etc.)
// relative to the last point.
// o ----> o ----> o <---- o.
// 1 2 3 4
// ^ isLastPoint = true.
// To calculate bearing for points 1 to 3 one have to go beardist from
// previous point to the next one (eg. from 1 to 2 and from 2 to 3).
// For point 4 one have to go from 4 to 3 reversing directions. And
// distance-to-next point is taken from point 3. You can learn more in
// TomTom OpenLR spec.
void GetStartLines(std::vector<m2::PointD> const & points, bool const isLastPoint, Graph::EdgeVector & edges);
void GetAllSuitablePaths(Graph::EdgeVector const & startLines, bool isLastPoint, double bearDistM,
FunctionalRoadClass functionalRoadClass, FormOfWay formOfWay, double distanceToNextPointM,
std::vector<LinkPtr> & allPaths);
void GetBestCandidatePaths(std::vector<LinkPtr> const & allPaths, bool const isLastPoint,
uint32_t const requiredBearing, double const bearDistM, m2::PointD const & startPoint,
std::vector<Graph::EdgeVector> & candidates);
void GetLineCandidates(openlr::LocationReferencePoint const & p, bool const isLastPoint,
double const distanceToNextPointM, std::vector<m2::PointD> const & pointCandidates,
std::vector<Graph::EdgeVector> & candidates);
CandidatePointsGetter & m_pointsGetter;
Graph & m_graph;
RoadInfoGetter & m_infoGetter;
v2::Stats & m_stats;
};
} // namespace openlr

View file

@ -0,0 +1,82 @@
#include "openlr/candidate_points_getter.hpp"
#include "openlr/helpers.hpp"
#include "routing/road_graph.hpp"
#include "storage/country_info_getter.hpp"
#include "geometry/point_with_altitude.hpp"
#include "base/stl_helpers.hpp"
#include <algorithm>
#include <utility>
#include <vector>
using namespace routing;
namespace openlr
{
void CandidatePointsGetter::FillJunctionPointCandidates(m2::PointD const & p, std::vector<m2::PointD> & candidates)
{
// TODO(mgsergio): Get optimal value using experiments on a sample.
// Or start with small radius and scale it up when there are too few points.
size_t const kRectSideMeters = 110;
auto const rect = mercator::RectByCenterXYAndSizeInMeters(p, kRectSideMeters);
auto const selectCandidates = [&rect, &candidates](FeatureType & ft)
{
ft.ParseGeometry(FeatureType::BEST_GEOMETRY);
ft.ForEachPoint([&rect, &candidates](m2::PointD const & candidate)
{
if (rect.IsPointInside(candidate))
candidates.emplace_back(candidate);
}, scales::GetUpperScale());
};
m_dataSource.ForEachInRect(selectCandidates, rect, scales::GetUpperScale());
// TODO: Move this to a separate stage.
// 1030292476 Does not match. Some problem occur with points.
// Either points duplicate or something alike. Check this
// later. The idea to fix this was to move SortUnique to the stage
// after enriching with projections.
base::SortUnique(candidates, [&p](m2::PointD const & a, m2::PointD const & b)
{ return mercator::DistanceOnEarth(a, p) < mercator::DistanceOnEarth(b, p); },
[](m2::PointD const & a, m2::PointD const & b) { return a == b; });
candidates.resize(std::min(m_maxJunctionCandidates, candidates.size()));
}
void CandidatePointsGetter::EnrichWithProjectionPoints(m2::PointD const & p, std::vector<m2::PointD> & candidates)
{
m_graph.ResetFakes();
std::vector<std::pair<Graph::Edge, geometry::PointWithAltitude>> vicinities;
m_graph.FindClosestEdges(p, static_cast<uint32_t>(m_maxProjectionCandidates), vicinities);
for (auto const & v : vicinities)
{
auto const & edge = v.first;
auto const & junction = v.second;
ASSERT(edge.HasRealPart() && !edge.IsFake(), ());
if (PointsAreClose(edge.GetStartPoint(), junction.GetPoint()) ||
PointsAreClose(edge.GetEndPoint(), junction.GetPoint()))
{
continue;
}
auto const firstHalf = Edge::MakeFake(edge.GetStartJunction(), junction, edge);
auto const secondHalf = Edge::MakeFake(junction, edge.GetEndJunction(), edge);
m_graph.AddOutgoingFakeEdge(firstHalf);
m_graph.AddIngoingFakeEdge(firstHalf);
m_graph.AddOutgoingFakeEdge(secondHalf);
m_graph.AddIngoingFakeEdge(secondHalf);
candidates.push_back(junction.GetPoint());
}
}
} // namespace openlr

View file

@ -0,0 +1,43 @@
#pragma once
#include "openlr/graph.hpp"
#include "openlr/stats.hpp"
#include "indexer/data_source.hpp"
#include "geometry/point2d.hpp"
#include <cstddef>
#include <functional>
#include <vector>
namespace openlr
{
class CandidatePointsGetter
{
public:
CandidatePointsGetter(size_t const maxJunctionCandidates, size_t const maxProjectionCandidates,
DataSource const & dataSource, Graph & graph)
: m_maxJunctionCandidates(maxJunctionCandidates)
, m_maxProjectionCandidates(maxProjectionCandidates)
, m_dataSource(dataSource)
, m_graph(graph)
{}
void GetCandidatePoints(m2::PointD const & p, std::vector<m2::PointD> & candidates)
{
FillJunctionPointCandidates(p, candidates);
EnrichWithProjectionPoints(p, candidates);
}
private:
void FillJunctionPointCandidates(m2::PointD const & p, std::vector<m2::PointD> & candidates);
void EnrichWithProjectionPoints(m2::PointD const & p, std::vector<m2::PointD> & candidates);
size_t const m_maxJunctionCandidates;
size_t const m_maxProjectionCandidates;
DataSource const & m_dataSource;
Graph & m_graph;
};
} // namespace openlr

View file

@ -0,0 +1,175 @@
#include "openlr/decoded_path.hpp"
#include "indexer/data_source.hpp"
#include "platform/country_file.hpp"
#include "geometry/latlon.hpp"
#include "geometry/mercator.hpp"
#include "base/assert.hpp"
#include "base/scope_guard.hpp"
#include "base/string_utils.hpp"
#include <sstream>
#define THROW_IF_NODE_IS_EMPTY(node, exc, msg) \
if (!node) \
MYTHROW(exc, msg)
namespace
{
uint32_t UintFromXML(pugi::xml_node const & node)
{
THROW_IF_NODE_IS_EMPTY(node, openlr::DecodedPathLoadError, ("Can't parse uint"));
return node.text().as_uint();
}
bool IsForwardFromXML(pugi::xml_node const & node)
{
THROW_IF_NODE_IS_EMPTY(node, openlr::DecodedPathLoadError, ("Can't parse IsForward"));
return node.text().as_bool();
}
void LatLonToXML(ms::LatLon const & latLon, pugi::xml_node & node)
{
auto const kDigitsAfterComma = 5;
node.append_child("lat").text() = strings::to_string_dac(latLon.m_lat, kDigitsAfterComma).data();
node.append_child("lon").text() = strings::to_string_dac(latLon.m_lon, kDigitsAfterComma).data();
}
void LatLonFromXML(pugi::xml_node const & node, ms::LatLon & latLon)
{
THROW_IF_NODE_IS_EMPTY(node, openlr::DecodedPathLoadError, ("Can't parse latLon"));
latLon.m_lat = node.child("lat").text().as_double();
latLon.m_lon = node.child("lon").text().as_double();
}
void FeatureIdFromXML(pugi::xml_node const & node, DataSource const & dataSource, FeatureID & fid)
{
THROW_IF_NODE_IS_EMPTY(node, openlr::DecodedPathLoadError, ("Can't parse CountryName"));
auto const countryName = node.child("CountryName").text().as_string();
fid.m_mwmId = dataSource.GetMwmIdByCountryFile(platform::CountryFile(countryName));
CHECK(fid.m_mwmId.IsAlive(), ("Can't get mwm id for country", countryName));
fid.m_index = node.child("Index").text().as_uint();
}
void FeatureIdToXML(FeatureID const & fid, pugi::xml_node & node)
{
node.append_child("CountryName").text() = fid.m_mwmId.GetInfo()->GetCountryName().data();
node.append_child("Index").text() = fid.m_index;
}
} // namespace
namespace openlr
{
uint32_t UintValueFromXML(pugi::xml_node const & node)
{
auto const value = node.child("olr:value");
if (!value)
return 0;
return UintFromXML(value);
}
void WriteAsMappingForSpark(std::string const & fileName, std::vector<DecodedPath> const & paths)
{
std::ofstream ofs(fileName);
if (!ofs.is_open())
MYTHROW(DecodedPathSaveError, ("Can't write to file", fileName, strerror(errno)));
WriteAsMappingForSpark(ofs, paths);
if (ofs.fail())
MYTHROW(DecodedPathSaveError, ("An error occured while writing file", fileName, strerror(errno)));
}
void WriteAsMappingForSpark(std::ostream & ost, std::vector<DecodedPath> const & paths)
{
auto const flags = ost.flags();
ost << std::fixed; // Avoid scientific notation cause '-' is used as fields separator.
SCOPE_GUARD(guard, ([&ost, &flags] { ost.flags(flags); }));
for (auto const & p : paths)
{
if (p.m_path.empty())
continue;
ost << p.m_segmentId.Get() << '\t';
auto const kFieldSep = '-';
auto const kSegmentSep = '=';
for (auto it = std::begin(p.m_path); it != std::end(p.m_path); ++it)
{
auto const & fid = it->GetFeatureId();
ost << fid.m_mwmId.GetInfo()->GetCountryName() << kFieldSep << fid.m_index << kFieldSep << it->GetSegId()
<< kFieldSep << (it->IsForward() ? "fwd" : "bwd") << kFieldSep
<< mercator::DistanceOnEarth(GetStart(*it), GetEnd(*it));
if (std::next(it) != std::end(p.m_path))
ost << kSegmentSep;
}
ost << std::endl;
}
}
void PathFromXML(pugi::xml_node const & node, DataSource const & dataSource, Path & p)
{
auto const edges = node.select_nodes("RoadEdge");
for (auto const & xmlE : edges)
{
auto e = xmlE.node();
FeatureID fid;
FeatureIdFromXML(e.child("FeatureID"), dataSource, fid);
auto const isForward = IsForwardFromXML(e.child("IsForward"));
auto const segmentId = UintFromXML(e.child("SegmentId"));
ms::LatLon start, end;
LatLonFromXML(e.child("StartJunction"), start);
LatLonFromXML(e.child("EndJunction"), end);
p.push_back(
Edge::MakeReal(fid, isForward, segmentId,
geometry::PointWithAltitude(mercator::FromLatLon(start), geometry::kDefaultAltitudeMeters),
geometry::PointWithAltitude(mercator::FromLatLon(end), geometry::kDefaultAltitudeMeters)));
}
}
void PathToXML(Path const & path, pugi::xml_node & node)
{
for (auto const & e : path)
{
auto edge = node.append_child("RoadEdge");
{
auto fid = edge.append_child("FeatureID");
FeatureIdToXML(e.GetFeatureId(), fid);
}
edge.append_child("IsForward").text() = e.IsForward();
edge.append_child("SegmentId").text() = e.GetSegId();
{
auto start = edge.append_child("StartJunction");
auto end = edge.append_child("EndJunction");
LatLonToXML(mercator::ToLatLon(GetStart(e)), start);
LatLonToXML(mercator::ToLatLon(GetEnd(e)), end);
}
}
}
} // namespace openlr
namespace routing
{
std::vector<m2::PointD> GetPoints(openlr::Path const & p)
{
CHECK(!p.empty(), ("Path should not be empty"));
std::vector<m2::PointD> points;
points.push_back(GetStart(p.front()));
for (auto const & e : p)
points.push_back(GetEnd(e));
return points;
}
} // namespace routing

View file

@ -0,0 +1,55 @@
#pragma once
#include "routing/road_graph.hpp"
#include "base/exception.hpp"
#include "base/newtype.hpp"
#include <cstdint>
#include <fstream>
#include <string>
#include <vector>
#include <pugixml.hpp>
class DataSource;
namespace openlr
{
using Path = routing::RoadGraphBase::EdgeVector;
using routing::Edge;
NEWTYPE(uint32_t, PartnerSegmentId);
NEWTYPE_SIMPLE_OUTPUT(PartnerSegmentId);
DECLARE_EXCEPTION(DecodedPathLoadError, RootException);
DECLARE_EXCEPTION(DecodedPathSaveError, RootException);
struct DecodedPath
{
PartnerSegmentId m_segmentId;
Path m_path;
};
uint32_t UintValueFromXML(pugi::xml_node const & node);
void WriteAsMappingForSpark(std::string const & fileName, std::vector<DecodedPath> const & paths);
void WriteAsMappingForSpark(std::ostream & ost, std::vector<DecodedPath> const & paths);
void PathFromXML(pugi::xml_node const & node, DataSource const & dataSource, Path & path);
void PathToXML(Path const & path, pugi::xml_node & node);
} // namespace openlr
namespace routing
{
inline m2::PointD GetStart(Edge const & e)
{
return e.GetStartJunction().GetPoint();
}
inline m2::PointD GetEnd(Edge const & e)
{
return e.GetEndJunction().GetPoint();
}
std::vector<m2::PointD> GetPoints(routing::RoadGraphBase::EdgeVector const & p);
} // namespace routing

86
tools/openlr/graph.cpp Normal file
View file

@ -0,0 +1,86 @@
#include "openlr/graph.hpp"
#include "geometry/mercator.hpp"
#include "geometry/point_with_altitude.hpp"
#include <map>
#include <memory>
#include <utility>
#include <vector>
using namespace routing;
using namespace std;
namespace openlr
{
namespace
{
using EdgeGetter = void (IRoadGraph::*)(geometry::PointWithAltitude const &, RoadGraphBase::EdgeListT &) const;
void GetRegularEdges(geometry::PointWithAltitude const & junction, IRoadGraph const & graph,
EdgeGetter const edgeGetter, Graph::EdgeCacheT & cache, Graph::EdgeListT & edges)
{
auto const it = cache.find(junction);
if (it == end(cache))
{
auto & es = cache[junction];
(graph.*edgeGetter)(junction, es);
edges.append(begin(es), end(es));
}
else
{
auto const & es = it->second;
edges.append(begin(es), end(es));
}
}
} // namespace
Graph::Graph(DataSource & dataSource, shared_ptr<CarModelFactory> carModelFactory)
: m_dataSource(dataSource, nullptr /* numMwmIDs */)
, m_graph(m_dataSource, IRoadGraph::Mode::ObeyOnewayTag, carModelFactory)
{}
void Graph::GetOutgoingEdges(Junction const & junction, EdgeListT & edges)
{
GetRegularOutgoingEdges(junction, edges);
m_graph.GetFakeOutgoingEdges(junction, edges);
}
void Graph::GetIngoingEdges(Junction const & junction, EdgeListT & edges)
{
GetRegularIngoingEdges(junction, edges);
m_graph.GetFakeIngoingEdges(junction, edges);
}
void Graph::GetRegularOutgoingEdges(Junction const & junction, EdgeListT & edges)
{
GetRegularEdges(junction, m_graph, &IRoadGraph::GetRegularOutgoingEdges, m_outgoingCache, edges);
}
void Graph::GetRegularIngoingEdges(Junction const & junction, EdgeListT & edges)
{
GetRegularEdges(junction, m_graph, &IRoadGraph::GetRegularIngoingEdges, m_ingoingCache, edges);
}
void Graph::FindClosestEdges(m2::PointD const & point, uint32_t const count,
vector<pair<Edge, Junction>> & vicinities) const
{
m_graph.FindClosestEdges(mercator::RectByCenterXYAndSizeInMeters(point, FeaturesRoadGraph::kClosestEdgesRadiusM),
count, vicinities);
}
void Graph::AddIngoingFakeEdge(Edge const & e)
{
m_graph.AddIngoingFakeEdge(e);
}
void Graph::AddOutgoingFakeEdge(Edge const & e)
{
m_graph.AddOutgoingFakeEdge(e);
}
void Graph::GetFeatureTypes(FeatureID const & featureId, feature::TypesHolder & types) const
{
m_graph.GetFeatureTypes(featureId, types);
}
} // namespace openlr

62
tools/openlr/graph.hpp Normal file
View file

@ -0,0 +1,62 @@
#pragma once
#include "routing/data_source.hpp"
#include "routing/features_road_graph.hpp"
#include "routing/road_graph.hpp"
#include "routing_common/car_model.hpp"
#include "indexer/feature_data.hpp"
#include "geometry/point2d.hpp"
#include <cstddef>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
namespace openlr
{
// TODO(mgsergio): Inherit from FeaturesRoadGraph.
class Graph
{
public:
using Edge = routing::Edge;
using EdgeListT = routing::FeaturesRoadGraph::EdgeListT;
using EdgeVector = routing::FeaturesRoadGraph::EdgeVector;
using Junction = geometry::PointWithAltitude;
Graph(DataSource & dataSource, std::shared_ptr<routing::CarModelFactory> carModelFactory);
// Appends edges such as that edge.GetStartJunction() == junction to the |edges|.
void GetOutgoingEdges(geometry::PointWithAltitude const & junction, EdgeListT & edges);
// Appends edges such as that edge.GetEndJunction() == junction to the |edges|.
void GetIngoingEdges(geometry::PointWithAltitude const & junction, EdgeListT & edges);
// Appends edges such as that edge.GetStartJunction() == junction and edge.IsFake() == false
// to the |edges|.
void GetRegularOutgoingEdges(Junction const & junction, EdgeListT & edges);
// Appends edges such as that edge.GetEndJunction() == junction and edge.IsFale() == false
// to the |edges|.
void GetRegularIngoingEdges(Junction const & junction, EdgeListT & edges);
void FindClosestEdges(m2::PointD const & point, uint32_t const count,
std::vector<std::pair<Edge, Junction>> & vicinities) const;
void AddIngoingFakeEdge(Edge const & e);
void AddOutgoingFakeEdge(Edge const & e);
void ResetFakes() { m_graph.ResetFakes(); }
void GetFeatureTypes(FeatureID const & featureId, feature::TypesHolder & types) const;
using EdgeCacheT = std::map<Junction, EdgeListT>;
private:
routing::MwmDataSource m_dataSource;
routing::FeaturesRoadGraph m_graph;
EdgeCacheT m_outgoingCache, m_ingoingCache;
};
} // namespace openlr

230
tools/openlr/helpers.cpp Normal file
View file

@ -0,0 +1,230 @@
#include "openlr/helpers.hpp"
#include "openlr/road_info_getter.hpp"
#include "routing/features_road_graph.hpp"
#include "geometry/mercator.hpp"
#include "base/stl_iterator.hpp"
#include <algorithm>
#include <optional>
#include <sstream>
namespace
{
using namespace openlr;
using namespace std;
openlr::FunctionalRoadClass HighwayClassToFunctionalRoadClass(ftypes::HighwayClass const & hwClass)
{
switch (hwClass)
{
case ftypes::HighwayClass::Motorway: return openlr::FunctionalRoadClass::FRC0;
case ftypes::HighwayClass::Trunk: return openlr::FunctionalRoadClass::FRC0;
case ftypes::HighwayClass::Primary: return openlr::FunctionalRoadClass::FRC1;
case ftypes::HighwayClass::Secondary: return openlr::FunctionalRoadClass::FRC2;
case ftypes::HighwayClass::Tertiary: return openlr::FunctionalRoadClass::FRC3;
case ftypes::HighwayClass::LivingStreet: return openlr::FunctionalRoadClass::FRC4;
case ftypes::HighwayClass::Service: return openlr::FunctionalRoadClass::FRC5;
case ftypes::HighwayClass::ServiceMinor: return openlr::FunctionalRoadClass::FRC6;
default: return openlr::FunctionalRoadClass::FRC7;
}
}
/// \returns nullopt if |e| doesn't conform to |functionalRoadClass| and score otherwise.
optional<Score> GetFrcScore(Graph::Edge const & e, FunctionalRoadClass functionalRoadClass, RoadInfoGetter & infoGetter)
{
CHECK(!e.IsFake(), ());
Score constexpr kMaxScoreForFrc = 25;
if (functionalRoadClass == FunctionalRoadClass::NotAValue)
return nullopt;
auto const hwClass = infoGetter.Get(e.GetFeatureId()).m_hwClass;
using ftypes::HighwayClass;
switch (functionalRoadClass)
{
case FunctionalRoadClass::FRC0:
return hwClass == HighwayClass::Motorway || hwClass == HighwayClass::Trunk ? optional<Score>(kMaxScoreForFrc) : nullopt;
case FunctionalRoadClass::FRC1:
return (hwClass == HighwayClass::Motorway || hwClass == HighwayClass::Trunk || hwClass == HighwayClass::Primary) ? optional<Score>(kMaxScoreForFrc)
: nullopt;
case FunctionalRoadClass::FRC2:
case FunctionalRoadClass::FRC3:
if (hwClass == HighwayClass::Secondary || hwClass == HighwayClass::Tertiary)
return optional<Score>(kMaxScoreForFrc);
return hwClass == HighwayClass::Primary || hwClass == HighwayClass::LivingStreet ? optional<Score>(0) : nullopt;
case FunctionalRoadClass::FRC4:
if (hwClass == HighwayClass::LivingStreet || hwClass == HighwayClass::Service)
return optional<Score>(kMaxScoreForFrc);
return (hwClass == HighwayClass::Tertiary || hwClass == HighwayClass::Secondary) ? optional<Score>(0) : nullopt;
case FunctionalRoadClass::FRC5:
case FunctionalRoadClass::FRC6:
case FunctionalRoadClass::FRC7:
return (hwClass == HighwayClass::LivingStreet || hwClass == HighwayClass::Service ||
hwClass == HighwayClass::ServiceMinor)
? optional<Score>(kMaxScoreForFrc)
: nullopt;
case FunctionalRoadClass::NotAValue: UNREACHABLE();
}
UNREACHABLE();
}
} // namespace
namespace openlr
{
// BearingPointsSelector ---------------------------------------------------------------------------
BearingPointsSelector::BearingPointsSelector(uint32_t bearDistM, bool isLastPoint)
: m_bearDistM(bearDistM)
, m_isLastPoint(isLastPoint)
{}
m2::PointD BearingPointsSelector::GetStartPoint(Graph::Edge const & e) const
{
return m_isLastPoint ? e.GetEndPoint() : e.GetStartPoint();
}
m2::PointD BearingPointsSelector::GetEndPoint(Graph::Edge const & e, double distanceM) const
{
if (distanceM < m_bearDistM && m_bearDistM <= distanceM + EdgeLength(e))
{
auto const edgeLen = EdgeLength(e);
auto const edgeBearDist = min(m_bearDistM - distanceM, edgeLen);
CHECK_LESS_OR_EQUAL(edgeBearDist, edgeLen, ());
return m_isLastPoint ? PointAtSegmentM(e.GetEndPoint(), e.GetStartPoint(), edgeBearDist)
: PointAtSegmentM(e.GetStartPoint(), e.GetEndPoint(), edgeBearDist);
}
return m_isLastPoint ? e.GetStartPoint() : e.GetEndPoint();
}
bool PointsAreClose(m2::PointD const & p1, m2::PointD const & p2)
{
double const kMwmRoadCrossingRadiusMeters = routing::GetRoadCrossingRadiusMeters();
return mercator::DistanceOnEarth(p1, p2) < kMwmRoadCrossingRadiusMeters;
}
double EdgeLength(Graph::Edge const & e)
{
return mercator::DistanceOnEarth(e.GetStartPoint(), e.GetEndPoint());
}
bool EdgesAreAlmostEqual(Graph::Edge const & e1, Graph::Edge const & e2)
{
// TODO(mgsergio): Do I need to check fields other than points?
return PointsAreClose(e1.GetStartPoint(), e2.GetStartPoint()) && PointsAreClose(e1.GetEndPoint(), e2.GetEndPoint());
}
string LogAs2GisPath(Graph::EdgeVector const & path)
{
CHECK(!path.empty(), ("Paths should not be empty"));
ostringstream ost;
ost << "https://2gis.ru/moscow?queryState=";
auto ll = mercator::ToLatLon(path.front().GetStartPoint());
ost << "center%2F" << ll.m_lon << "%2C" << ll.m_lat << "%2F";
ost << "zoom%2F" << 17 << "%2F";
ost << "ruler%2Fpoints%2F";
for (auto const & e : path)
{
ll = mercator::ToLatLon(e.GetStartPoint());
ost << ll.m_lon << "%20" << ll.m_lat << "%2C";
}
ll = mercator::ToLatLon(path.back().GetEndPoint());
ost << ll.m_lon << "%20" << ll.m_lat;
return ost.str();
}
string LogAs2GisPath(Graph::Edge const & e)
{
return LogAs2GisPath(Graph::EdgeVector({e}));
}
bool PassesRestriction(Graph::Edge const & e, FunctionalRoadClass restriction, FormOfWay formOfWay, int frcThreshold,
RoadInfoGetter & infoGetter)
{
if (e.IsFake() || restriction == FunctionalRoadClass::NotAValue)
return true;
auto const frc = HighwayClassToFunctionalRoadClass(infoGetter.Get(e.GetFeatureId()).m_hwClass);
return static_cast<int>(frc) <= static_cast<int>(restriction) + frcThreshold;
}
bool PassesRestrictionV3(Graph::Edge const & e, FunctionalRoadClass functionalRoadClass, FormOfWay formOfWay,
RoadInfoGetter & infoGetter, Score & score)
{
CHECK(!e.IsFake(), ("Edges should not be fake:", e));
auto const frcScore = GetFrcScore(e, functionalRoadClass, infoGetter);
if (!frcScore)
return false;
score = *frcScore;
Score constexpr kScoreForFormOfWay = 25;
if (formOfWay == FormOfWay::Roundabout && infoGetter.Get(e.GetFeatureId()).m_isRoundabout)
score += kScoreForFormOfWay;
return true;
}
bool ConformLfrcnp(Graph::Edge const & e, FunctionalRoadClass lowestFrcToNextPoint, int frcThreshold,
RoadInfoGetter & infoGetter)
{
if (e.IsFake() || lowestFrcToNextPoint == FunctionalRoadClass::NotAValue)
return true;
auto const frc = HighwayClassToFunctionalRoadClass(infoGetter.Get(e.GetFeatureId()).m_hwClass);
return static_cast<int>(frc) <= static_cast<int>(lowestFrcToNextPoint) + frcThreshold;
}
bool ConformLfrcnpV3(Graph::Edge const & e, FunctionalRoadClass lowestFrcToNextPoint, RoadInfoGetter & infoGetter)
{
return GetFrcScore(e, lowestFrcToNextPoint, infoGetter).has_value();
}
size_t IntersectionLen(Graph::EdgeVector a, Graph::EdgeVector b)
{
sort(a.begin(), a.end());
sort(b.begin(), b.end());
return set_intersection(a.begin(), a.end(), b.begin(), b.end(), CounterIterator()).GetCount();
}
bool SuffixEqualsPrefix(Graph::EdgeVector const & a, Graph::EdgeVector const & b, size_t len)
{
CHECK_LESS_OR_EQUAL(len, a.size(), ());
CHECK_LESS_OR_EQUAL(len, b.size(), ());
return equal(a.end() - len, a.end(), b.begin());
}
// Returns a length of the longest suffix of |a| that matches any prefix of |b|.
// Neither |a| nor |b| can contain several repetitions of any edge.
// Returns -1 if |a| intersection |b| is not equal to some suffix of |a| and some prefix of |b|.
int32_t PathOverlappingLen(Graph::EdgeVector const & a, Graph::EdgeVector const & b)
{
auto const len = IntersectionLen(a, b);
if (SuffixEqualsPrefix(a, b, len))
return base::checked_cast<int32_t>(len);
return -1;
}
m2::PointD PointAtSegmentM(m2::PointD const & p1, m2::PointD const & p2, double const distanceM)
{
auto const v = p2 - p1;
auto const l = v.Length();
auto const L = mercator::DistanceOnEarth(p1, p2);
auto const delta = distanceM * l / L;
return PointAtSegment(p1, p2, delta);
}
} // namespace openlr

71
tools/openlr/helpers.hpp Normal file
View file

@ -0,0 +1,71 @@
#pragma once
#include "openlr/graph.hpp"
#include "openlr/openlr_model.hpp"
#include "openlr/score_types.hpp"
#include "geometry/point2d.hpp"
#include <cstdint>
#include <string>
#include <type_traits>
namespace openlr
{
class RoadInfoGetter;
// This class is used to get points for further bearing calculations.
class BearingPointsSelector
{
public:
BearingPointsSelector(uint32_t bearDistM, bool isLastPoint);
m2::PointD GetStartPoint(Graph::Edge const & e) const;
m2::PointD GetEndPoint(Graph::Edge const & e, double distanceM) const;
private:
double m_bearDistM;
bool m_isLastPoint;
};
bool PointsAreClose(m2::PointD const & p1, m2::PointD const & p2);
double EdgeLength(Graph::Edge const & e);
bool EdgesAreAlmostEqual(Graph::Edge const & e1, Graph::Edge const & e2);
// TODO(mgsergio): Remove when unused.
std::string LogAs2GisPath(Graph::EdgeVector const & path);
std::string LogAs2GisPath(Graph::Edge const & e);
template <typename T, typename U, std::enable_if_t<!(std::is_signed<T>::value ^ std::is_signed<U>::value), int> = 0>
std::common_type_t<T, U> AbsDifference(T const a, U const b)
{
return a >= b ? a - b : b - a;
}
bool PassesRestriction(Graph::Edge const & e, FunctionalRoadClass restriction, FormOfWay formOfWay, int frcThreshold,
RoadInfoGetter & infoGetter);
/// \returns true if |e| conforms |functionalRoadClass| and |formOfWay| and false otherwise.
/// \note If the method returns true |score| should be considered next.
bool PassesRestrictionV3(Graph::Edge const & e, FunctionalRoadClass functionalRoadClass, FormOfWay formOfWay,
RoadInfoGetter & infoGetter, Score & score);
/// \returns true if edge |e| conforms Lowest Functional Road Class to Next Point.
/// \note frc means Functional Road Class. Please see openlr documentation for details:
/// http://www.openlr.org/data/docs/whitepaper/1_0/OpenLR-Whitepaper_v1.0.pdf
bool ConformLfrcnp(Graph::Edge const & e, FunctionalRoadClass lowestFrcToNextPoint, int frcThreshold,
RoadInfoGetter & infoGetter);
bool ConformLfrcnpV3(Graph::Edge const & e, FunctionalRoadClass lowestFrcToNextPoint, RoadInfoGetter & infoGetter);
size_t IntersectionLen(Graph::EdgeVector a, Graph::EdgeVector b);
bool SuffixEqualsPrefix(Graph::EdgeVector const & a, Graph::EdgeVector const & b, size_t len);
// Returns a length of the longest suffix of |a| that matches any prefix of |b|.
// Neither |a| nor |b| can contain several repetitions of any edge.
// Returns -1 if |a| intersection |b| is not equal to some suffix of |a| and some prefix of |b|.
int32_t PathOverlappingLen(Graph::EdgeVector const & a, Graph::EdgeVector const & b);
m2::PointD PointAtSegmentM(m2::PointD const & p1, m2::PointD const & p2, double const distanceM);
} // namespace openlr

View file

@ -0,0 +1,499 @@
#include "openlr/openlr_decoder.hpp"
#include "openlr/cache_line_size.hpp"
#include "openlr/candidate_paths_getter.hpp"
#include "openlr/candidate_points_getter.hpp"
#include "openlr/decoded_path.hpp"
#include "openlr/graph.hpp"
#include "openlr/helpers.hpp"
#include "openlr/openlr_model.hpp"
#include "openlr/paths_connector.hpp"
#include "openlr/road_info_getter.hpp"
#include "openlr/router.hpp"
#include "openlr/score_candidate_paths_getter.hpp"
#include "openlr/score_candidate_points_getter.hpp"
#include "openlr/score_paths_connector.hpp"
#include "openlr/score_types.hpp"
#include "openlr/way_point.hpp"
#include "routing/features_road_graph.hpp"
#include "routing/road_graph.hpp"
#include "routing_common/car_model.hpp"
#include "storage/country_info_getter.hpp"
#include "indexer/classificator.hpp"
#include "indexer/data_source.hpp"
#include "platform/country_file.hpp"
#include "geometry/mercator.hpp"
#include "geometry/parametrized_segment.hpp"
#include "geometry/point2d.hpp"
#include "geometry/polyline2d.hpp"
#include "base/logging.hpp"
#include "base/math.hpp"
#include "base/stl_helpers.hpp"
#include "base/timer.hpp"
#include <algorithm>
#include <chrono>
#include <fstream>
#include <functional>
#include <iterator>
#include <memory>
#include <queue>
#include <thread>
#include <utility>
using namespace routing;
using namespace std;
namespace openlr
{
namespace
{
struct alignas(kCacheLineSize) Stats
{
void Add(Stats const & rhs)
{
m_shortRoutes += rhs.m_shortRoutes;
m_zeroCanditates += rhs.m_zeroCanditates;
m_moreThanOneCandidate += rhs.m_moreThanOneCandidate;
m_routesFailed += rhs.m_routesFailed;
m_tightOffsets += rhs.m_tightOffsets;
m_routesHandled += rhs.m_routesHandled;
}
void Report() const
{
LOG(LINFO, ("Parsed segments:", m_routesHandled));
LOG(LINFO, ("Routes failed:", m_routesFailed));
LOG(LINFO, ("Tight offsets:", m_tightOffsets));
LOG(LINFO, ("Short routes:", m_shortRoutes));
LOG(LINFO, ("Ambiguous routes:", m_moreThanOneCandidate));
LOG(LINFO, ("Path is not reconstructed:", m_zeroCanditates));
}
uint32_t m_shortRoutes = 0;
uint32_t m_zeroCanditates = 0;
uint32_t m_moreThanOneCandidate = 0;
uint32_t m_routesFailed = 0;
uint32_t m_tightOffsets = 0;
uint32_t m_routesHandled = 0;
};
bool IsRealVertex(m2::PointD const & p, FeatureID const & fid, DataSource const & dataSource)
{
FeaturesLoaderGuard g(dataSource, fid.m_mwmId);
auto const ft = g.GetOriginalFeatureByIndex(fid.m_index);
bool matched = false;
ft->ForEachPoint([&p, &matched](m2::PointD const & fp)
{
if (p == fp)
matched = true;
}, FeatureType::BEST_GEOMETRY);
return matched;
}
void ExpandFake(Graph::EdgeVector & path, Graph::EdgeVector::iterator edgeIt, DataSource const & dataSource, Graph & g)
{
if (!edgeIt->IsFake())
return;
Graph::EdgeListT edges;
bool startIsFake = true;
if (IsRealVertex(edgeIt->GetStartPoint(), edgeIt->GetFeatureId(), dataSource))
{
g.GetRegularOutgoingEdges(edgeIt->GetStartJunction(), edges);
startIsFake = false;
}
else
{
ASSERT(IsRealVertex(edgeIt->GetEndPoint(), edgeIt->GetFeatureId(), dataSource), ());
g.GetRegularIngoingEdges(edgeIt->GetEndJunction(), edges);
}
CHECK(!edges.empty(), ());
auto it = find_if(begin(edges), end(edges), [&edgeIt](Graph::Edge const & real)
{
if (real.GetFeatureId() == edgeIt->GetFeatureId() && real.GetSegId() == edgeIt->GetSegId())
return true;
return false;
});
// For features which cross mwm border FeatureIds may not match. Check geometry.
if (it == end(edges))
{
it = find_if(begin(edges), end(edges), [&edgeIt, &startIsFake](Graph::Edge const & real)
{
// Features from the same mwm should be already matched.
if (real.GetFeatureId().m_mwmId == edgeIt->GetFeatureId().m_mwmId)
return false;
auto const fakePoint = startIsFake ? edgeIt->GetStartPoint() : edgeIt->GetEndPoint();
m2::ParametrizedSegment<m2::PointD> const realGeometry(real.GetStartPoint(), real.GetEndPoint());
auto const projectedPoint = realGeometry.ClosestPointTo(fakePoint);
auto constexpr kCrossMwmMatchDistanceM = 1.0;
if (mercator::DistanceOnEarth(fakePoint, projectedPoint) < kCrossMwmMatchDistanceM)
return true;
return false;
});
}
CHECK(it != end(edges), ());
// If a fake edge is larger than a half of the corresponding real one, substitute
// the fake one with real one. Drop the fake one otherwize.
if (2 * EdgeLength(*edgeIt) >= EdgeLength(*it))
*edgeIt = *it;
else
path.erase(edgeIt);
}
void ExpandFakes(DataSource const & dataSource, Graph & g, Graph::EdgeVector & path)
{
ASSERT(!path.empty(), ());
ExpandFake(path, std::begin(path), dataSource, g);
if (path.empty())
return;
ExpandFake(path, std::end(path) - 1, dataSource, g);
}
// Returns an iterator pointing to the first edge that should not be cut off.
// Offsets denote a distance in meters one should travel from the start/end of the path
// to some point along that path and drop everything form the start to that point or from
// that point to the end.
template <typename InputIterator>
InputIterator CutOffset(InputIterator start, InputIterator stop, double offset, bool keepEnd)
{
if (offset == 0.0)
return start;
for (double distance = 0.0; start != stop; ++start)
{
auto const edgeLen = EdgeLength(*start);
if (distance <= offset && offset < distance + edgeLen)
{
// Throw out this edge if (offset - distance) is greater than edgeLength / 2.
if (!keepEnd && offset - distance >= edgeLen / 2.0)
++start;
break;
}
distance += edgeLen;
}
return start;
}
template <typename InputIterator, typename OutputIterator>
void CopyWithoutOffsets(InputIterator start, InputIterator stop, OutputIterator out, uint32_t positiveOffset,
uint32_t negativeOffset, bool keepEnds)
{
auto from = start;
auto to = stop;
if (distance(start, stop) > 1)
{
from = CutOffset(start, stop, positiveOffset, keepEnds);
// |to| points past the last edge we need to take.
to = CutOffset(reverse_iterator<InputIterator>(stop), reverse_iterator<InputIterator>(start), negativeOffset,
keepEnds)
.base();
}
if (!keepEnds)
CHECK(from <= to, ("From iterator is less or equal than to."));
if (from >= to)
return;
copy(from, to, out);
}
class SegmentsDecoderV2
{
public:
SegmentsDecoderV2(DataSource & dataSource, unique_ptr<CarModelFactory> cmf)
: m_dataSource(dataSource)
, m_graph(dataSource, std::move(cmf))
, m_infoGetter(dataSource)
{}
bool DecodeSegment(LinearSegment const & segment, DecodedPath & path, v2::Stats & stat)
{
double constexpr kPathLengthTolerance = 0.30;
uint32_t constexpr kMaxJunctionCandidates = 10;
uint32_t constexpr kMaxProjectionCandidates = 5;
m_graph.ResetFakes();
path.m_segmentId.Set(segment.m_segmentId);
auto const & points = segment.GetLRPs();
CHECK_GREATER(points.size(), 1, ("A segment cannot consist of less than two points"));
vector<vector<Graph::EdgeVector>> lineCandidates;
lineCandidates.reserve(points.size());
LOG(LDEBUG, ("Decoding segment:", segment.m_segmentId, "with", points.size(), "points"));
CandidatePointsGetter pointsGetter(kMaxJunctionCandidates, kMaxProjectionCandidates, m_dataSource, m_graph);
CandidatePathsGetter pathsGetter(pointsGetter, m_graph, m_infoGetter, stat);
if (!pathsGetter.GetLineCandidatesForPoints(points, lineCandidates))
return false;
vector<Graph::EdgeVector> resultPath;
PathsConnector connector(kPathLengthTolerance, m_graph, m_infoGetter, stat);
if (!connector.ConnectCandidates(points, lineCandidates, resultPath))
return false;
Graph::EdgeVector route;
for (auto const & part : resultPath)
route.insert(end(route), begin(part), end(part));
double requiredRouteDistanceM = 0.0;
// Sum app all distances between points. Last point's m_distanceToNextPoint
// should be equal to zero, but let's skip it just in case.
CHECK(!points.empty(), ());
for (auto it = begin(points); it != prev(end(points)); ++it)
requiredRouteDistanceM += it->m_distanceToNextPoint;
double actualRouteDistanceM = 0.0;
for (auto const & e : route)
actualRouteDistanceM += EdgeLength(e);
auto const scale = actualRouteDistanceM / requiredRouteDistanceM;
LOG(LDEBUG, ("actualRouteDistance:", actualRouteDistanceM, "requiredRouteDistance:", requiredRouteDistanceM,
"scale:", scale));
auto const positiveOffsetM = segment.m_locationReference.m_positiveOffsetMeters * scale;
auto const negativeOffsetM = segment.m_locationReference.m_negativeOffsetMeters * scale;
if (positiveOffsetM + negativeOffsetM >= requiredRouteDistanceM)
{
++stat.m_wrongOffsets;
LOG(LINFO, ("Wrong offsets for segment:", segment.m_segmentId));
return false;
}
ExpandFakes(m_dataSource, m_graph, route);
ASSERT(none_of(begin(route), end(route), mem_fn(&Graph::Edge::IsFake)), (segment.m_segmentId));
CopyWithoutOffsets(begin(route), end(route), back_inserter(path.m_path), positiveOffsetM, negativeOffsetM,
false /* keep ends */);
if (path.m_path.empty())
{
++stat.m_wrongOffsets;
LOG(LINFO, ("Path is empty after offsets cutting. segmentId:", segment.m_segmentId));
return false;
}
return true;
}
private:
DataSource const & m_dataSource;
Graph m_graph;
RoadInfoGetter m_infoGetter;
};
// The idea behind the third version of matching algorithm is to collect a lot of candidates (paths)
// which correspond an openlr edges with some score. And on the final stage to decide which
// candidate is better.
class SegmentsDecoderV3
{
public:
SegmentsDecoderV3(DataSource & dataSource, unique_ptr<CarModelFactory> carModelFactory)
: m_dataSource(dataSource)
, m_graph(dataSource, std::move(carModelFactory))
, m_infoGetter(dataSource)
{}
bool DecodeSegment(LinearSegment const & segment, DecodedPath & path, v2::Stats & stat)
{
LOG(LINFO, ("DecodeSegment(...) seg id:", segment.m_segmentId, ", point num:", segment.GetLRPs().size()));
uint32_t constexpr kMaxJunctionCandidates = 10;
uint32_t constexpr kMaxProjectionCandidates = 5;
path.m_segmentId.Set(segment.m_segmentId);
auto const & points = segment.GetLRPs();
CHECK_GREATER(points.size(), 1, ("A segment cannot consist of less than two points"));
vector<ScorePathVec> lineCandidates;
lineCandidates.reserve(points.size());
LOG(LINFO, ("Decoding segment:", segment.m_segmentId, "with", points.size(), "points"));
ScoreCandidatePointsGetter pointsGetter(kMaxJunctionCandidates, kMaxProjectionCandidates, m_dataSource, m_graph);
ScoreCandidatePathsGetter pathsGetter(pointsGetter, m_graph, m_infoGetter, stat);
if (!pathsGetter.GetLineCandidatesForPoints(points, segment.m_source, lineCandidates))
return false;
vector<Graph::EdgeVector> resultPath;
ScorePathsConnector connector(m_graph, m_infoGetter, stat);
if (!connector.FindBestPath(points, lineCandidates, segment.m_source, resultPath))
{
LOG(LINFO, ("Connections not found:", segment.m_segmentId));
auto const mercatorPoints = segment.GetMercatorPoints();
for (auto const & mercatorPoint : mercatorPoints)
LOG(LINFO, (mercator::ToLatLon(mercatorPoint)));
return false;
}
Graph::EdgeVector route;
for (auto const & part : resultPath)
route.insert(route.end(), part.begin(), part.end());
double requiredRouteDistanceM = 0.0;
// Sum up all distances between points. Last point's m_distanceToNextPoint
// should be equal to zero, but let's skip it just in case.
CHECK(!points.empty(), ());
for (auto it = points.begin(); it != prev(points.end()); ++it)
requiredRouteDistanceM += it->m_distanceToNextPoint;
double actualRouteDistanceM = 0.0;
for (auto const & e : route)
actualRouteDistanceM += EdgeLength(e);
auto const scale = actualRouteDistanceM / requiredRouteDistanceM;
LOG(LINFO, ("actualRouteDistance:", actualRouteDistanceM, "requiredRouteDistance:", requiredRouteDistanceM,
"scale:", scale));
if (segment.m_locationReference.m_positiveOffsetMeters + segment.m_locationReference.m_negativeOffsetMeters >=
requiredRouteDistanceM)
{
++stat.m_wrongOffsets;
LOG(LINFO, ("Wrong offsets for segment:", segment.m_segmentId));
return false;
}
auto const positiveOffsetM = segment.m_locationReference.m_positiveOffsetMeters * scale;
auto const negativeOffsetM = segment.m_locationReference.m_negativeOffsetMeters * scale;
CHECK(none_of(route.begin(), route.end(), mem_fn(&Graph::Edge::IsFake)), (segment.m_segmentId));
CopyWithoutOffsets(route.begin(), route.end(), back_inserter(path.m_path), positiveOffsetM, negativeOffsetM,
true /* keep ends */);
if (path.m_path.empty())
{
++stat.m_wrongOffsets;
LOG(LINFO, ("Path is empty after offsets cutting. segmentId:", segment.m_segmentId));
return false;
}
return true;
}
private:
DataSource const & m_dataSource;
Graph m_graph;
RoadInfoGetter m_infoGetter;
};
size_t constexpr GetOptimalBatchSize()
{
// This code computes the most optimal (in the sense of cache lines
// occupancy) batch size.
size_t constexpr a = math::LCM(sizeof(LinearSegment), kCacheLineSize) / sizeof(LinearSegment);
size_t constexpr b = math::LCM(sizeof(IRoadGraph::EdgeVector), kCacheLineSize) / sizeof(IRoadGraph::EdgeVector);
return math::LCM(a, b);
}
} // namespace
// OpenLRDecoder::SegmentsFilter -------------------------------------------------------------
OpenLRDecoder::SegmentsFilter::SegmentsFilter(string const & idsPath, bool const multipointsOnly)
: m_idsSet(false)
, m_multipointsOnly(multipointsOnly)
{
if (idsPath.empty())
return;
ifstream ifs(idsPath);
CHECK(ifs, ("Can't find", idsPath));
m_ids.insert(istream_iterator<uint32_t>(ifs), istream_iterator<uint32_t>());
CHECK(!ifs, ("Garbage in", idsPath));
m_idsSet = true;
}
bool OpenLRDecoder::SegmentsFilter::Matches(LinearSegment const & segment) const
{
if (m_multipointsOnly && segment.m_locationReference.m_points.size() == 2)
return false;
if (m_idsSet && m_ids.count(segment.m_segmentId) == 0)
return false;
return true;
}
// OpenLRDecoder -----------------------------------------------------------------------------
OpenLRDecoder::OpenLRDecoder(vector<FrozenDataSource> & dataSources,
CountryParentNameGetter const & countryParentNameGetter)
: m_dataSources(dataSources)
, m_countryParentNameGetter(countryParentNameGetter)
{}
void OpenLRDecoder::DecodeV2(vector<LinearSegment> const & segments, uint32_t const numThreads,
vector<DecodedPath> & paths)
{
Decode<SegmentsDecoderV2, v2::Stats>(segments, numThreads, paths);
}
void OpenLRDecoder::DecodeV3(vector<LinearSegment> const & segments, uint32_t numThreads, vector<DecodedPath> & paths)
{
Decode<SegmentsDecoderV3, v2::Stats>(segments, numThreads, paths);
}
template <typename Decoder, typename Stats>
void OpenLRDecoder::Decode(vector<LinearSegment> const & segments, uint32_t const numThreads,
vector<DecodedPath> & paths)
{
auto const worker = [&](size_t threadNum, DataSource & dataSource, Stats & stat)
{
size_t constexpr kBatchSize = GetOptimalBatchSize();
size_t constexpr kProgressFrequency = 100;
size_t const numSegments = segments.size();
Decoder decoder(dataSource, make_unique<CarModelFactory>(m_countryParentNameGetter));
base::Timer timer;
for (size_t i = threadNum * kBatchSize; i < numSegments; i += numThreads * kBatchSize)
{
for (size_t j = i; j < numSegments && j < i + kBatchSize; ++j)
{
if (!decoder.DecodeSegment(segments[j], paths[j], stat))
++stat.m_routesFailed;
++stat.m_routesHandled;
if (stat.m_routesHandled % kProgressFrequency == 0 || i == segments.size() - 1)
{
LOG(LINFO, ("Thread", threadNum, "processed", stat.m_routesHandled, "failed:", stat.m_routesFailed));
timer.Reset();
}
}
}
};
base::Timer timer;
vector<Stats> stats(numThreads);
vector<thread> workers;
for (size_t i = 1; i < numThreads; ++i)
workers.emplace_back(worker, i, ref(m_dataSources[i]), ref(stats[i]));
worker(0 /* threadNum */, m_dataSources[0], stats[0]);
for (auto & worker : workers)
worker.join();
Stats allStats;
for (auto const & s : stats)
allStats.Add(s);
allStats.Report();
LOG(LINFO, ("Matching tool:", timer.ElapsedSeconds(), "seconds."));
}
} // namespace openlr

View file

@ -0,0 +1,58 @@
#pragma once
#include "openlr/stats.hpp"
#include "indexer/data_source.hpp"
#include "base/exception.hpp"
#include <cstdint>
#include <functional>
#include <string>
#include <unordered_set>
#include <vector>
namespace openlr
{
struct LinearSegment;
struct DecodedPath;
DECLARE_EXCEPTION(DecoderError, RootException);
class Graph;
class RoadInfoGetter;
class OpenLRDecoder
{
public:
using CountryParentNameGetter = std::function<std::string(std::string const &)>;
class SegmentsFilter
{
public:
SegmentsFilter(std::string const & idsPath, bool const multipointsOnly);
bool Matches(LinearSegment const & segment) const;
private:
std::unordered_set<uint32_t> m_ids;
bool m_idsSet;
bool const m_multipointsOnly;
};
OpenLRDecoder(std::vector<FrozenDataSource> & dataSources, CountryParentNameGetter const & countryParentNameGetter);
// Maps partner segments to mwm paths. |segments| should be sorted by partner id.
void DecodeV2(std::vector<LinearSegment> const & segments, uint32_t const numThreads,
std::vector<DecodedPath> & paths);
void DecodeV3(std::vector<LinearSegment> const & segments, uint32_t numThreads, std::vector<DecodedPath> & paths);
private:
template <typename Decoder, typename Stats>
void Decode(std::vector<LinearSegment> const & segments, uint32_t const numThreads, std::vector<DecodedPath> & paths);
std::vector<FrozenDataSource> & m_dataSources;
CountryParentNameGetter m_countryParentNameGetter;
};
} // namespace openlr

View file

@ -0,0 +1,5 @@
project(openlr_match_quality)
if (NOT SKIP_QT_GUI)
add_subdirectory(openlr_assessment_tool)
endif()

View file

@ -0,0 +1,31 @@
project(openlr_assessment_tool)
set(SRC
main.cpp
mainwindow.cpp
mainwindow.hpp
map_widget.cpp
map_widget.hpp
points_controller_delegate_base.hpp
segment_correspondence.cpp
segment_correspondence.hpp
traffic_drawer_delegate_base.hpp
traffic_mode.cpp
traffic_mode.hpp
traffic_panel.cpp
traffic_panel.hpp
trafficmodeinitdlg.cpp
trafficmodeinitdlg.h
trafficmodeinitdlg.ui
)
omim_add_executable(${PROJECT_NAME} ${SRC})
set_target_properties(${PROJECT_NAME} PROPERTIES AUTOUIC ON AUTOMOC ON)
target_link_libraries(${PROJECT_NAME}
openlr
qt_common
map
gflags::gflags
)

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<plist>
<dict>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHighResolutionCapable</key>
<string>True</string>
</dict>
</plist>

View file

@ -0,0 +1,41 @@
#include "mainwindow.hpp"
#include "qt/qt_common/helpers.hpp"
#include "map/framework.hpp"
#include <gflags/gflags.h>
#include <QtWidgets/QApplication>
namespace
{
DEFINE_string(resources_path, "", "Path to resources directory");
DEFINE_string(data_path, "", "Path to data directory");
} // namespace
int main(int argc, char * argv[])
{
gflags::SetUsageMessage("Visualize and check matched routes.");
gflags::ParseCommandLineFlags(&argc, &argv, true);
Platform & platform = GetPlatform();
if (!FLAGS_resources_path.empty())
platform.SetResourceDir(FLAGS_resources_path);
if (!FLAGS_data_path.empty())
platform.SetWritableDirForTests(FLAGS_data_path);
Q_INIT_RESOURCE(resources_common);
QApplication app(argc, argv);
qt::common::SetDefaultSurfaceFormat(app.platformName());
FrameworkParams params;
Framework framework(params);
openlr::MainWindow mainWindow(framework);
mainWindow.showMaximized();
return app.exec();
}

View file

@ -0,0 +1,374 @@
#include "openlr/openlr_match_quality/openlr_assessment_tool/mainwindow.hpp"
#include "openlr/openlr_match_quality/openlr_assessment_tool/map_widget.hpp"
#include "openlr/openlr_match_quality/openlr_assessment_tool/points_controller_delegate_base.hpp"
#include "openlr/openlr_match_quality/openlr_assessment_tool/traffic_drawer_delegate_base.hpp"
#include "openlr/openlr_match_quality/openlr_assessment_tool/traffic_panel.hpp"
#include "openlr/openlr_match_quality/openlr_assessment_tool/trafficmodeinitdlg.h"
#include "map/framework.hpp"
#include "drape_frontend/drape_api.hpp"
#include "routing/data_source.hpp"
#include "routing/features_road_graph.hpp"
#include "routing/road_graph.hpp"
#include "routing_common/car_model.hpp"
#include "storage/country_parent_getter.hpp"
#include "geometry/mercator.hpp"
#include "geometry/point2d.hpp"
#include <QApplication>
#include <QClipboard>
#include <QDockWidget>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QKeySequence>
#include <QLayout>
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
#include <QStandardPaths>
#include <cerrno>
#include <cstring>
#include <memory>
#include <vector>
namespace openlr
{
namespace
{
class TrafficDrawerDelegate : public TrafficDrawerDelegateBase
{
static constexpr char const * kEncodedLineId = "encodedPath";
static constexpr char const * kDecodedLineId = "decodedPath";
static constexpr char const * kGoldenLineId = "goldenPath";
public:
explicit TrafficDrawerDelegate(Framework & framework)
: m_framework(framework)
, m_drapeApi(m_framework.GetDrapeApi())
, m_bm(framework.GetBookmarkManager())
{}
void SetViewportCenter(m2::PointD const & center) override { m_framework.SetViewportCenter(center); }
void DrawDecodedSegments(std::vector<m2::PointD> const & points) override
{
CHECK(!points.empty(), ("Points must not be empty."));
LOG(LINFO, ("Decoded segment", points));
m_drapeApi.AddLine(
kDecodedLineId,
df::DrapeApiLineData(points, dp::Color(0, 0, 255, 255)).Width(3.0f).ShowPoints(true /* markPoints */));
}
void DrawEncodedSegment(std::vector<m2::PointD> const & points) override
{
LOG(LINFO, ("Encoded segment", points));
m_drapeApi.AddLine(
kEncodedLineId,
df::DrapeApiLineData(points, dp::Color(255, 0, 0, 255)).Width(3.0f).ShowPoints(true /* markPoints */));
}
void DrawGoldenPath(std::vector<m2::PointD> const & points) override
{
m_drapeApi.AddLine(
kGoldenLineId,
df::DrapeApiLineData(points, dp::Color(255, 127, 36, 255)).Width(4.0f).ShowPoints(true /* markPoints */));
}
void ClearGoldenPath() override { m_drapeApi.RemoveLine(kGoldenLineId); }
void ClearAllPaths() override { m_drapeApi.Clear(); }
void VisualizePoints(std::vector<m2::PointD> const & points) override
{
auto editSession = m_bm.GetEditSession();
editSession.SetIsVisible(UserMark::Type::DEBUG_MARK, true);
for (auto const & p : points)
editSession.CreateUserMark<DebugMarkPoint>(p);
}
void ClearAllVisualizedPoints() override { m_bm.GetEditSession().ClearGroup(UserMark::Type::DEBUG_MARK); }
private:
Framework & m_framework;
df::DrapeApi & m_drapeApi;
BookmarkManager & m_bm;
};
bool PointsMatch(m2::PointD const & a, m2::PointD const & b)
{
auto constexpr kToleranceDistanceM = 1.0;
return mercator::DistanceOnEarth(a, b) < kToleranceDistanceM;
}
class PointsControllerDelegate : public PointsControllerDelegateBase
{
public:
explicit PointsControllerDelegate(Framework & framework)
: m_framework(framework)
, m_dataSource(const_cast<DataSource &>(GetDataSource()), nullptr /* numMwmIDs */)
, m_roadGraph(m_dataSource, routing::IRoadGraph::Mode::ObeyOnewayTag,
std::make_unique<routing::CarModelFactory>(storage::CountryParentGetter{}))
{}
std::vector<m2::PointD> GetAllJunctionPointsInViewport() const override
{
std::vector<m2::PointD> points;
auto const & rect = m_framework.GetCurrentViewport();
auto const pushPoint = [&points, &rect](m2::PointD const & point)
{
if (!rect.IsPointInside(point))
return;
for (auto const & p : points)
if (PointsMatch(point, p))
return;
points.push_back(point);
};
auto const pushFeaturePoints = [&pushPoint](FeatureType & ft)
{
if (ft.GetGeomType() != feature::GeomType::Line)
return;
/// @todo Transported (railway=rail) are also present here :)
auto const roadClass = ftypes::GetHighwayClass(feature::TypesHolder(ft));
if (roadClass == ftypes::HighwayClass::Undefined || roadClass == ftypes::HighwayClass::Pedestrian)
return;
ft.ForEachPoint(pushPoint, scales::GetUpperScale());
};
GetDataSource().ForEachInRect(pushFeaturePoints, rect, scales::GetUpperScale());
return points;
}
std::pair<std::vector<FeaturePoint>, m2::PointD> GetCandidatePoints(m2::PointD const & p) const override
{
auto constexpr kInvalidIndex = std::numeric_limits<size_t>::max();
std::vector<FeaturePoint> points;
m2::PointD pointOnFt;
indexer::ForEachFeatureAtPoint(GetDataSource(), [&points, &p, &pointOnFt](FeatureType & ft)
{
if (ft.GetGeomType() != feature::GeomType::Line)
return;
ft.ParseGeometry(FeatureType::BEST_GEOMETRY);
auto minDistance = std::numeric_limits<double>::max();
auto bestPointIndex = kInvalidIndex;
for (size_t i = 0; i < ft.GetPointsCount(); ++i)
{
auto const & fp = ft.GetPoint(i);
auto const distance = mercator::DistanceOnEarth(fp, p);
if (PointsMatch(fp, p) && distance < minDistance)
{
bestPointIndex = i;
minDistance = distance;
}
}
if (bestPointIndex != kInvalidIndex)
{
points.emplace_back(ft.GetID(), bestPointIndex);
pointOnFt = ft.GetPoint(bestPointIndex);
}
}, p);
return std::make_pair(points, pointOnFt);
}
std::vector<m2::PointD> GetReachablePoints(m2::PointD const & p) const override
{
routing::FeaturesRoadGraph::EdgeListT edges;
m_roadGraph.GetOutgoingEdges(geometry::PointWithAltitude(p, geometry::kDefaultAltitudeMeters), edges);
std::vector<m2::PointD> points;
for (auto const & e : edges)
points.push_back(e.GetEndJunction().GetPoint());
return points;
}
ClickType CheckClick(m2::PointD const & clickPoint, m2::PointD const & lastClickedPoint,
std::vector<m2::PointD> const & reachablePoints) const override
{
// == Comparison is safe here since |clickPoint| is adjusted by GetFeaturesPointsByPoint
// so to be equal the closest feature's one.
if (clickPoint == lastClickedPoint)
return ClickType::Remove;
for (auto const & p : reachablePoints)
if (PointsMatch(clickPoint, p))
return ClickType::Add;
return ClickType::Miss;
}
private:
DataSource const & GetDataSource() const { return m_framework.GetDataSource(); }
Framework & m_framework;
routing::MwmDataSource m_dataSource;
routing::FeaturesRoadGraph m_roadGraph;
};
} // namespace
MainWindow::MainWindow(Framework & framework) : m_framework(framework)
{
m_mapWidget = new MapWidget(m_framework, this /* parent */);
m_layout = new QHBoxLayout();
m_layout->addWidget(m_mapWidget);
auto * window = new QWidget();
window->setLayout(m_layout);
window->setGraphicsEffect(nullptr);
setCentralWidget(window);
setWindowTitle(tr("Organic Maps"));
setWindowIcon(QIcon(":/ui/logo.png"));
QMenu * fileMenu = new QMenu("File", this);
menuBar()->addMenu(fileMenu);
fileMenu->addAction("Open sample", QKeySequence("Ctrl+O"), this, &MainWindow::OnOpenTrafficSample);
m_closeTrafficSampleAction =
fileMenu->addAction("Close sample", QKeySequence("Ctrl+W"), this, &MainWindow::OnCloseTrafficSample);
m_saveTrafficSampleAction =
fileMenu->addAction("Save sample", QKeySequence("Ctrl+S"), this, &MainWindow::OnSaveTrafficSample);
fileMenu->addSeparator();
m_goldifyMatchedPathAction =
fileMenu->addAction("Goldify", QKeySequence("Ctrl+G"), [this] { m_trafficMode->GoldifyMatchedPath(); });
m_startEditingAction = fileMenu->addAction("Edit", QKeySequence("Ctrl+E"), [this]
{
m_trafficMode->StartBuildingPath();
m_mapWidget->SetMode(MapWidget::Mode::TrafficMarkup);
m_commitPathAction->setEnabled(true /* enabled */);
m_cancelPathAction->setEnabled(true /* enabled */);
});
m_commitPathAction = fileMenu->addAction("Accept path", QKeySequence("Ctrl+A"), [this]
{
m_trafficMode->CommitPath();
m_mapWidget->SetMode(MapWidget::Mode::Normal);
});
m_cancelPathAction = fileMenu->addAction("Revert path", QKeySequence("Ctrl+R"), [this]
{
m_trafficMode->RollBackPath();
m_mapWidget->SetMode(MapWidget::Mode::Normal);
});
m_ignorePathAction = fileMenu->addAction("Ignore path", QKeySequence("Ctrl+I"), [this]
{
m_trafficMode->IgnorePath();
m_mapWidget->SetMode(MapWidget::Mode::Normal);
});
m_goldifyMatchedPathAction->setEnabled(false /* enabled */);
m_closeTrafficSampleAction->setEnabled(false /* enabled */);
m_saveTrafficSampleAction->setEnabled(false /* enabled */);
m_startEditingAction->setEnabled(false /* enabled */);
m_commitPathAction->setEnabled(false /* enabled */);
m_cancelPathAction->setEnabled(false /* enabled */);
m_ignorePathAction->setEnabled(false /* enabled */);
}
void MainWindow::CreateTrafficPanel(std::string const & dataFilePath)
{
m_trafficMode =
new TrafficMode(dataFilePath, m_framework.GetDataSource(), std::make_unique<TrafficDrawerDelegate>(m_framework),
std::make_unique<PointsControllerDelegate>(m_framework));
connect(m_mapWidget, &MapWidget::TrafficMarkupClick, m_trafficMode, &TrafficMode::OnClick);
connect(m_trafficMode, &TrafficMode::EditingStopped, this, &MainWindow::OnPathEditingStop);
connect(m_trafficMode, &TrafficMode::SegmentSelected,
[](int segmentId) { QApplication::clipboard()->setText(QString::number(segmentId)); });
m_docWidget = new QDockWidget(tr("Routes"), this);
addDockWidget(Qt::DockWidgetArea::RightDockWidgetArea, m_docWidget);
m_docWidget->setWidget(new TrafficPanel(m_trafficMode, m_docWidget));
m_docWidget->adjustSize();
m_docWidget->setMinimumWidth(400);
m_docWidget->show();
}
void MainWindow::DestroyTrafficPanel()
{
removeDockWidget(m_docWidget);
delete m_docWidget;
m_docWidget = nullptr;
delete m_trafficMode;
m_trafficMode = nullptr;
m_mapWidget->SetMode(MapWidget::Mode::Normal);
}
void MainWindow::OnOpenTrafficSample()
{
TrafficModeInitDlg dlg;
dlg.exec();
if (dlg.result() != QDialog::DialogCode::Accepted)
return;
try
{
CreateTrafficPanel(dlg.GetDataFilePath());
}
catch (TrafficModeError const & e)
{
QMessageBox::critical(this, "Data loading error", QString("Can't load data file."));
LOG(LERROR, (e.Msg()));
return;
}
m_goldifyMatchedPathAction->setEnabled(true /* enabled */);
m_closeTrafficSampleAction->setEnabled(true /* enabled */);
m_saveTrafficSampleAction->setEnabled(true /* enabled */);
m_startEditingAction->setEnabled(true /* enabled */);
m_ignorePathAction->setEnabled(true /* enabled */);
}
void MainWindow::OnCloseTrafficSample()
{
// TODO(mgsergio):
// If not saved, ask a user if he/she wants to save.
// OnSaveTrafficSample()
m_goldifyMatchedPathAction->setEnabled(false /* enabled */);
m_saveTrafficSampleAction->setEnabled(false /* enabled */);
m_closeTrafficSampleAction->setEnabled(false /* enabled */);
m_startEditingAction->setEnabled(false /* enabled */);
m_commitPathAction->setEnabled(false /* enabled */);
m_cancelPathAction->setEnabled(false /* enabled */);
m_ignorePathAction->setEnabled(false /* enabled */);
DestroyTrafficPanel();
}
void MainWindow::OnSaveTrafficSample()
{
// TODO(mgsergio): Add default filename.
auto const & fileName = QFileDialog::getSaveFileName(this, "Save sample");
if (fileName.isEmpty())
return;
if (!m_trafficMode->SaveSampleAs(fileName.toStdString()))
QMessageBox::critical(this, "Saving error", QString("Can't save file: ") + strerror(errno));
}
void MainWindow::OnPathEditingStop()
{
m_commitPathAction->setEnabled(false /* enabled */);
m_cancelPathAction->setEnabled(false /* enabled */);
m_cancelPathAction->setEnabled(false /* enabled */);
}
} // namespace openlr

View file

@ -0,0 +1,62 @@
#pragma once
#include "openlr/openlr_match_quality/openlr_assessment_tool/traffic_mode.hpp"
#include "base/string_utils.hpp"
#include <string>
#include <QMainWindow>
class Framework;
class QHBoxLayout;
namespace openlr
{
class MapWidget;
class TrafficMode;
class WebView;
} // namespace openlr
namespace df
{
class DrapeApi;
}
class QDockWidget;
namespace openlr
{
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(Framework & framework);
private:
void CreateTrafficPanel(std::string const & dataFilePath);
void DestroyTrafficPanel();
void OnOpenTrafficSample();
void OnCloseTrafficSample();
void OnSaveTrafficSample();
void OnPathEditingStop();
Framework & m_framework;
openlr::TrafficMode * m_trafficMode = nullptr;
QDockWidget * m_docWidget = nullptr;
QAction * m_goldifyMatchedPathAction = nullptr;
QAction * m_saveTrafficSampleAction = nullptr;
QAction * m_closeTrafficSampleAction = nullptr;
QAction * m_startEditingAction = nullptr;
QAction * m_commitPathAction = nullptr;
QAction * m_cancelPathAction = nullptr;
QAction * m_ignorePathAction = nullptr;
openlr::MapWidget * m_mapWidget = nullptr;
QHBoxLayout * m_layout = nullptr;
};
} // namespace openlr

View file

@ -0,0 +1,26 @@
#include "openlr/openlr_match_quality/openlr_assessment_tool/map_widget.hpp"
#include "qt/qt_common/helpers.hpp"
#include "map/framework.hpp"
#include <QMouseEvent>
namespace openlr
{
MapWidget::MapWidget(Framework & framework, QWidget * parent) : Base(framework, false /* screenshotMode */, parent) {}
void MapWidget::mousePressEvent(QMouseEvent * e)
{
Base::mousePressEvent(e);
if (qt::common::IsRightButton(e))
ShowInfoPopup(e, GetDevicePoint(e));
if (m_mode == Mode::TrafficMarkup)
{
auto pt = GetDevicePoint(e);
emit TrafficMarkupClick(m_framework.PtoG(pt), e->button());
}
}
} // namespace openlr

View file

@ -0,0 +1,43 @@
#pragma once
#include "qt/qt_common/map_widget.hpp"
namespace
{
class PointsController;
} // namespace
class Framework;
namespace openlr
{
class MapWidget : public qt::common::MapWidget
{
Q_OBJECT
using Base = qt::common::MapWidget;
public:
enum class Mode
{
Normal,
TrafficMarkup
};
MapWidget(Framework & framework, QWidget * parent);
~MapWidget() override = default;
void SetMode(Mode const mode) { m_mode = mode; }
QSize sizeHint() const override { return QSize(800, 600); }
signals:
void TrafficMarkupClick(m2::PointD const & p, Qt::MouseButton const b);
protected:
void mousePressEvent(QMouseEvent * e) override;
private:
Mode m_mode = Mode::Normal;
};
} // namespace openlr

View file

@ -0,0 +1,38 @@
#pragma once
#include "indexer/feature.hpp"
#include "geometry/point2d.hpp"
#include <cstddef>
#include <vector>
namespace openlr
{
using FeaturePoint = std::pair<FeatureID, size_t>;
/// This class is responsible for collecting junction points and
/// checking user's clicks.
class PointsControllerDelegateBase
{
public:
enum class ClickType
{
Miss,
Add,
Remove
};
virtual ~PointsControllerDelegateBase() = default;
virtual std::vector<m2::PointD> GetAllJunctionPointsInViewport() const = 0;
/// Returns all junction points at a given location in the form of feature id and
/// point index in the feature.
virtual std::pair<std::vector<FeaturePoint>, m2::PointD> GetCandidatePoints(m2::PointD const & p) const = 0;
// Returns all points that are one step reachable from |p|.
virtual std::vector<m2::PointD> GetReachablePoints(m2::PointD const & p) const = 0;
virtual ClickType CheckClick(m2::PointD const & clickPoint, m2::PointD const & lastClickedPoint,
std::vector<m2::PointD> const & reachablePoints) const = 0;
};
} // namespace openlr

View file

@ -0,0 +1,50 @@
#include "openlr/openlr_match_quality/openlr_assessment_tool/segment_correspondence.hpp"
namespace openlr
{
SegmentCorrespondence::SegmentCorrespondence(SegmentCorrespondence const & sc)
{
m_partnerSegment = sc.m_partnerSegment;
m_positiveOffset = sc.m_positiveOffset;
m_negativeOffset = sc.m_negativeOffset;
m_matchedPath = sc.m_matchedPath;
m_fakePath = sc.m_fakePath;
m_goldenPath = sc.m_goldenPath;
m_partnerXMLDoc.reset(sc.m_partnerXMLDoc);
m_partnerXMLSegment = m_partnerXMLDoc.child("reportSegments");
m_status = sc.m_status;
}
SegmentCorrespondence::SegmentCorrespondence(openlr::LinearSegment const & segment, uint32_t positiveOffset,
uint32_t negativeOffset, openlr::Path const & matchedPath,
openlr::Path const & fakePath, openlr::Path const & goldenPath,
pugi::xml_node const & partnerSegmentXML)
: m_partnerSegment(segment)
, m_positiveOffset(positiveOffset)
, m_negativeOffset(negativeOffset)
, m_matchedPath(matchedPath)
, m_fakePath(fakePath)
{
SetGoldenPath(goldenPath);
m_partnerXMLDoc.append_copy(partnerSegmentXML);
m_partnerXMLSegment = m_partnerXMLDoc.child("reportSegments");
CHECK(m_partnerXMLSegment, ("Node should contain <reportSegments> part"));
}
void SegmentCorrespondence::SetGoldenPath(openlr::Path const & p)
{
m_goldenPath = p;
m_status = p.empty() ? Status::Untouched : Status::Assessed;
}
void SegmentCorrespondence::Ignore()
{
m_status = Status::Ignored;
m_goldenPath.clear();
}
} // namespace openlr

View file

@ -0,0 +1,68 @@
#pragma once
#include "openlr/decoded_path.hpp"
#include "openlr/openlr_model.hpp"
#include <pugixml.hpp>
namespace openlr
{
class SegmentCorrespondence
{
public:
enum class Status
{
Untouched,
Assessed,
Ignored
};
SegmentCorrespondence(SegmentCorrespondence const & sc);
SegmentCorrespondence(openlr::LinearSegment const & segment, uint32_t positiveOffset, uint32_t negativeOffset,
openlr::Path const & matchedPath, openlr::Path const & fakePath,
openlr::Path const & goldenPath, pugi::xml_node const & partnerSegmentXML);
openlr::Path const & GetMatchedPath() const { return m_matchedPath; }
bool HasMatchedPath() const { return !m_matchedPath.empty(); }
uint32_t GetPositiveOffset() const { return m_positiveOffset; }
uint32_t GetNegativeOffset() const { return m_negativeOffset; }
openlr::Path const & GetFakePath() const { return m_fakePath; }
bool HasFakePath() const { return !m_fakePath.empty(); }
openlr::Path const & GetGoldenPath() const { return m_goldenPath; }
bool HasGoldenPath() const { return !m_goldenPath.empty(); }
void SetGoldenPath(openlr::Path const & p);
openlr::LinearSegment const & GetPartnerSegment() const { return m_partnerSegment; }
uint32_t GetPartnerSegmentId() const { return m_partnerSegment.m_segmentId; }
pugi::xml_document const & GetPartnerXML() const { return m_partnerXMLDoc; }
pugi::xml_node const & GetPartnerXMLSegment() const { return m_partnerXMLSegment; }
Status GetStatus() const { return m_status; }
void Ignore();
private:
openlr::LinearSegment m_partnerSegment;
uint32_t m_positiveOffset = 0;
uint32_t m_negativeOffset = 0;
openlr::Path m_matchedPath;
openlr::Path m_fakePath;
openlr::Path m_goldenPath;
// A dirty hack to save back SegmentCorrespondence.
// TODO(mgsergio): Consider unifying xml serialization with one used in openlr_stat.
pugi::xml_document m_partnerXMLDoc;
// This is used by GetPartnerXMLSegment shortcut to return const ref. pugi::xml_node is
// just a wrapper so returning by value won't guarantee constness.
pugi::xml_node m_partnerXMLSegment;
Status m_status = Status::Untouched;
};
} // namespace openlr

View file

@ -0,0 +1,25 @@
#pragma once
#include <geometry/point2d.hpp>
namespace openlr
{
/// This class is used to delegate segments drawing to the DrapeEngine.
class TrafficDrawerDelegateBase
{
public:
virtual ~TrafficDrawerDelegateBase() = default;
virtual void SetViewportCenter(m2::PointD const & center) = 0;
virtual void DrawDecodedSegments(std::vector<m2::PointD> const & points) = 0;
virtual void DrawEncodedSegment(std::vector<m2::PointD> const & points) = 0;
virtual void DrawGoldenPath(std::vector<m2::PointD> const & points) = 0;
virtual void ClearGoldenPath() = 0;
virtual void ClearAllPaths() = 0;
virtual void VisualizePoints(std::vector<m2::PointD> const & points) = 0;
virtual void ClearAllVisualizedPoints() = 0;
};
} // namespace openlr

View file

@ -0,0 +1,548 @@
#include "traffic_mode.hpp"
#include "openlr/openlr_model_xml.hpp"
#include "indexer/data_source.hpp"
#include "base/assert.hpp"
#include "base/scope_guard.hpp"
#include <QItemSelection>
#include <QMessageBox>
namespace openlr
{
namespace
{
void RemovePointFromPull(m2::PointD const & toBeRemoved, std::vector<m2::PointD> & pool)
{
pool.erase(remove_if(begin(pool), end(pool),
[&toBeRemoved](m2::PointD const & p) { return p.EqualDxDy(toBeRemoved, 1e-6); }),
end(pool));
}
std::vector<m2::PointD> GetReachablePoints(m2::PointD const & srcPoint, std::vector<m2::PointD> const path,
PointsControllerDelegateBase const & pointsDelegate,
size_t const lookbackIndex)
{
auto reachablePoints = pointsDelegate.GetReachablePoints(srcPoint);
if (lookbackIndex < path.size())
{
auto const & toBeRemoved = path[path.size() - lookbackIndex - 1];
RemovePointFromPull(toBeRemoved, reachablePoints);
}
return reachablePoints;
}
} // namespace
namespace impl
{
// static
size_t const RoadPointCandidate::kInvalidId = std::numeric_limits<size_t>::max();
/// This class denotes a "non-deterministic" feature point.
/// I.e. it is a set of all pairs <FeatureID, point index>
/// located at a specified coordinate.
/// Only one point at a time is considered active.
RoadPointCandidate::RoadPointCandidate(std::vector<FeaturePoint> const & points, m2::PointD const & coord)
: m_coord(coord)
, m_points(points)
{
LOG(LDEBUG, ("Candidate points:", points));
}
void RoadPointCandidate::ActivateCommonPoint(RoadPointCandidate const & rpc)
{
for (auto const & fp1 : m_points)
{
for (auto const & fp2 : rpc.m_points)
{
if (fp1.first == fp2.first)
{
SetActivePoint(fp1.first);
return;
}
}
}
CHECK(false, ("One common feature id should exist."));
}
FeaturePoint const & RoadPointCandidate::GetPoint() const
{
CHECK_NOT_EQUAL(m_activePointIndex, kInvalidId, ("No point is active."));
return m_points[m_activePointIndex];
}
m2::PointD const & RoadPointCandidate::GetCoordinate() const
{
return m_coord;
}
void RoadPointCandidate::SetActivePoint(FeatureID const & fid)
{
for (size_t i = 0; i < m_points.size(); ++i)
{
if (m_points[i].first == fid)
{
m_activePointIndex = i;
return;
}
}
CHECK(false, ("One point should match."));
}
} // namespace impl
// TrafficMode -------------------------------------------------------------------------------------
TrafficMode::TrafficMode(std::string const & dataFileName, DataSource const & dataSource,
std::unique_ptr<TrafficDrawerDelegateBase> drawerDelegate,
std::unique_ptr<PointsControllerDelegateBase> pointsDelegate, QObject * parent)
: QAbstractTableModel(parent)
, m_dataSource(dataSource)
, m_drawerDelegate(std::move(drawerDelegate))
, m_pointsDelegate(std::move(pointsDelegate))
{
// TODO(mgsergio): Collect stat how many segments of each kind were parsed.
pugi::xml_document doc;
if (!doc.load_file(dataFileName.data()))
MYTHROW(TrafficModeError, ("Can't load file:", strerror(errno)));
// Save root node without children.
{
auto const root = doc.document_element();
auto node = m_template.append_child(root.name());
for (auto const & attr : root.attributes())
node.append_copy(attr);
}
// Select all Segment elements that are direct children of the root.
auto const segments = doc.document_element().select_nodes("./Segment");
try
{
for (auto const & xpathNode : segments)
{
auto const xmlSegment = xpathNode.node();
openlr::Path matchedPath;
openlr::Path fakePath;
openlr::Path goldenPath;
openlr::LinearSegment segment;
// TODO(mgsergio): Unify error handling interface of openlr_xml_mode and decoded_path parsers.
auto const partnerSegmentXML = xmlSegment.child("reportSegments");
if (!openlr::SegmentFromXML(partnerSegmentXML, segment))
MYTHROW(TrafficModeError, ("An error occurred while parsing: can't parse segment"));
if (auto const route = xmlSegment.child("Route"))
openlr::PathFromXML(route, m_dataSource, matchedPath);
if (auto const route = xmlSegment.child("FakeRoute"))
openlr::PathFromXML(route, m_dataSource, fakePath);
if (auto const route = xmlSegment.child("GoldenRoute"))
openlr::PathFromXML(route, m_dataSource, goldenPath);
uint32_t positiveOffsetM = 0;
uint32_t negativeOffsetM = 0;
if (auto const reportSegmentLRC = partnerSegmentXML.child("ReportSegmentLRC"))
{
if (auto const method = reportSegmentLRC.child("method"))
{
if (auto const locationReference = method.child("olr:locationReference"))
{
if (auto const optionLinearLocationReference = locationReference.child("olr:optionLinearLocationReference"))
{
if (auto const positiveOffset = optionLinearLocationReference.child("olr:positiveOffset"))
positiveOffsetM = UintValueFromXML(positiveOffset);
if (auto const negativeOffset = optionLinearLocationReference.child("olr:negativeOffset"))
negativeOffsetM = UintValueFromXML(negativeOffset);
}
}
}
}
m_segments.emplace_back(segment, positiveOffsetM, negativeOffsetM, matchedPath, fakePath, goldenPath,
partnerSegmentXML);
if (auto const status = xmlSegment.child("Ignored"))
{
if (status.text().as_bool())
m_segments.back().Ignore();
}
}
}
catch (openlr::DecodedPathLoadError const & e)
{
MYTHROW(TrafficModeError, ("An exception occurred while parsing", dataFileName, e.Msg()));
}
LOG(LINFO, (segments.size(), "segments are loaded."));
}
// TODO(mgsergio): Check if a path was committed, or commit it.
bool TrafficMode::SaveSampleAs(std::string const & fileName) const
{
CHECK(!fileName.empty(), ("Can't save to an empty file."));
pugi::xml_document result;
result.reset(m_template);
auto root = result.document_element();
for (auto const & sc : m_segments)
{
auto segment = root.append_child("Segment");
segment.append_copy(sc.GetPartnerXMLSegment());
if (sc.GetStatus() == SegmentCorrespondence::Status::Ignored)
segment.append_child("Ignored").text() = true;
if (sc.HasMatchedPath())
{
auto node = segment.append_child("Route");
openlr::PathToXML(sc.GetMatchedPath(), node);
}
if (sc.HasFakePath())
{
auto node = segment.append_child("FakeRoute");
openlr::PathToXML(sc.GetFakePath(), node);
}
if (sc.HasGoldenPath())
{
auto node = segment.append_child("GoldenRoute");
openlr::PathToXML(sc.GetGoldenPath(), node);
}
}
result.save_file(fileName.data(), " " /* indent */);
return true;
}
int TrafficMode::rowCount(QModelIndex const & parent) const
{
return static_cast<int>(m_segments.size());
}
int TrafficMode::columnCount(QModelIndex const & parent) const
{
return 4;
}
QVariant TrafficMode::data(QModelIndex const & index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.row() >= rowCount())
return QVariant();
if (role != Qt::DisplayRole && role != Qt::EditRole)
return QVariant();
if (index.column() == 0)
return m_segments[index.row()].GetPartnerSegmentId();
if (index.column() == 1)
return static_cast<int>(m_segments[index.row()].GetStatus());
if (index.column() == 2)
return m_segments[index.row()].GetPositiveOffset();
if (index.column() == 3)
return m_segments[index.row()].GetNegativeOffset();
return QVariant();
}
QVariant TrafficMode::headerData(int section, Qt::Orientation orientation, int role /* = Qt::DisplayRole */) const
{
if (orientation != Qt::Horizontal && role != Qt::DisplayRole)
return QVariant();
switch (section)
{
case 0: return "Segment id"; break;
case 1: return "Status code"; break;
case 2: return "Positive offset (Meters)"; break;
case 3: return "Negative offset (Meters)"; break;
}
UNREACHABLE();
}
void TrafficMode::OnItemSelected(QItemSelection const & selected, QItemSelection const &)
{
ASSERT(!selected.empty(), ());
ASSERT(!m_segments.empty(), ());
auto const row = selected.front().top();
CHECK_LESS(static_cast<size_t>(row), m_segments.size(), ());
m_currentSegment = &m_segments[row];
auto const & partnerSegment = m_currentSegment->GetPartnerSegment();
auto const & partnerSegmentPoints = partnerSegment.GetMercatorPoints();
auto const & viewportCenter = partnerSegmentPoints.front();
m_drawerDelegate->ClearAllPaths();
// TODO(mgsergio): Use a better way to set viewport and scale.
m_drawerDelegate->SetViewportCenter(viewportCenter);
m_drawerDelegate->DrawEncodedSegment(partnerSegmentPoints);
if (m_currentSegment->HasMatchedPath())
m_drawerDelegate->DrawDecodedSegments(GetPoints(m_currentSegment->GetMatchedPath()));
if (m_currentSegment->HasGoldenPath())
m_drawerDelegate->DrawGoldenPath(GetPoints(m_currentSegment->GetGoldenPath()));
emit SegmentSelected(static_cast<int>(partnerSegment.m_segmentId));
}
Qt::ItemFlags TrafficMode::flags(QModelIndex const & index) const
{
if (!index.isValid())
return Qt::ItemIsEnabled;
return QAbstractItemModel::flags(index);
}
void TrafficMode::GoldifyMatchedPath()
{
if (!m_currentSegment->HasMatchedPath())
{
QMessageBox::information(nullptr /* parent */, "Error", "The selected segment does not have a matched path");
return;
}
if (!StartBuildingPathChecks())
return;
m_currentSegment->SetGoldenPath(m_currentSegment->GetMatchedPath());
m_goldenPath.clear();
m_drawerDelegate->DrawGoldenPath(GetPoints(m_currentSegment->GetGoldenPath()));
}
void TrafficMode::StartBuildingPath()
{
if (!StartBuildingPathChecks())
return;
m_currentSegment->SetGoldenPath({});
m_buildingPath = true;
m_drawerDelegate->ClearGoldenPath();
m_drawerDelegate->VisualizePoints(m_pointsDelegate->GetAllJunctionPointsInViewport());
}
void TrafficMode::PushPoint(m2::PointD const & coord, std::vector<FeaturePoint> const & points)
{
impl::RoadPointCandidate point(points, coord);
if (!m_goldenPath.empty())
m_goldenPath.back().ActivateCommonPoint(point);
m_goldenPath.push_back(point);
}
void TrafficMode::PopPoint()
{
CHECK(!m_goldenPath.empty(), ("Attempt to pop point from an empty path."));
m_goldenPath.pop_back();
}
void TrafficMode::CommitPath()
{
CHECK(m_currentSegment, ("No segments selected"));
if (!m_buildingPath)
MYTHROW(TrafficModeError, ("Path building is not started"));
SCOPE_GUARD(guard, [this] { emit EditingStopped(); });
m_buildingPath = false;
m_drawerDelegate->ClearAllVisualizedPoints();
if (m_goldenPath.size() == 1)
{
LOG(LDEBUG, ("Golden path is empty"));
return;
}
CHECK_GREATER(m_goldenPath.size(), 1, ("Path cannot consist of only one point"));
// Activate last point. Since no more points will be availabe we link it to the same
// feature as the previous one was linked to.
m_goldenPath.back().ActivateCommonPoint(m_goldenPath[GetPointsCount() - 2]);
openlr::Path path;
for (size_t i = 1; i < GetPointsCount(); ++i)
{
auto const prevPoint = m_goldenPath[i - 1];
auto point = m_goldenPath[i];
// The start and the end of the edge should lie on the same feature.
point.ActivateCommonPoint(prevPoint);
auto const & prevFt = prevPoint.GetPoint();
auto const & ft = point.GetPoint();
path.push_back(Edge::MakeReal(ft.first, prevFt.second < ft.second /* forward */,
base::checked_cast<uint32_t>(prevFt.second),
geometry::PointWithAltitude(prevPoint.GetCoordinate(), 0 /* altitude */),
geometry::PointWithAltitude(point.GetCoordinate(), 0 /* altitude */)));
}
m_currentSegment->SetGoldenPath(path);
m_goldenPath.clear();
}
void TrafficMode::RollBackPath()
{
CHECK(m_currentSegment, ("No segments selected"));
CHECK(m_buildingPath, ("No path building is in progress."));
m_buildingPath = false;
// TODO(mgsergio): Add a method for common visual manipulations.
m_drawerDelegate->ClearAllVisualizedPoints();
m_drawerDelegate->ClearGoldenPath();
if (m_currentSegment->HasGoldenPath())
m_drawerDelegate->DrawGoldenPath(GetPoints(m_currentSegment->GetGoldenPath()));
m_goldenPath.clear();
emit EditingStopped();
}
void TrafficMode::IgnorePath()
{
CHECK(m_currentSegment, ("No segments selected"));
if (m_currentSegment->HasGoldenPath())
{
auto const btn = QMessageBox::question(nullptr /* parent */, "Override warning",
"The selected segment has a golden path. Do you want to discard it?");
if (btn == QMessageBox::No)
return;
}
m_buildingPath = false;
// TODO(mgsergio): Add a method for common visual manipulations.
m_drawerDelegate->ClearAllVisualizedPoints();
m_drawerDelegate->ClearGoldenPath();
m_currentSegment->Ignore();
m_goldenPath.clear();
emit EditingStopped();
}
size_t TrafficMode::GetPointsCount() const
{
return m_goldenPath.size();
}
m2::PointD const & TrafficMode::GetPoint(size_t const index) const
{
return m_goldenPath[index].GetCoordinate();
}
m2::PointD const & TrafficMode::GetLastPoint() const
{
CHECK(!m_goldenPath.empty(), ("Attempt to get point from an empty path."));
return m_goldenPath.back().GetCoordinate();
}
std::vector<m2::PointD> TrafficMode::GetGoldenPathPoints() const
{
std::vector<m2::PointD> coordinates;
for (auto const & roadPoint : m_goldenPath)
coordinates.push_back(roadPoint.GetCoordinate());
return coordinates;
}
// TODO(mgsergio): Draw the first point when the path size is 1.
void TrafficMode::HandlePoint(m2::PointD clickPoint, Qt::MouseButton const button)
{
if (!m_buildingPath)
return;
auto const currentPathLength = GetPointsCount();
auto const lastClickedPoint = currentPathLength != 0 ? GetLastPoint() : m2::PointD::Zero();
auto const & p = m_pointsDelegate->GetCandidatePoints(clickPoint);
auto const & candidatePoints = p.first;
clickPoint = p.second;
if (candidatePoints.empty())
return;
auto reachablePoints =
GetReachablePoints(clickPoint, GetGoldenPathPoints(), *m_pointsDelegate, 0 /* lookBackIndex */);
auto const & clickablePoints =
currentPathLength != 0
? GetReachablePoints(lastClickedPoint, GetGoldenPathPoints(), *m_pointsDelegate, 1 /* lookbackIndex */)
// TODO(mgsergio): This is not quite correct since view port can change
// since first call to visualize points. But it's ok in general.
: m_pointsDelegate->GetAllJunctionPointsInViewport();
using ClickType = PointsControllerDelegateBase::ClickType;
switch (m_pointsDelegate->CheckClick(clickPoint, lastClickedPoint, clickablePoints))
{
case ClickType::Add:
// TODO(mgsergio): Think of refactoring this with if (accumulator.empty)
// instead of pushing point first ad then removing last selection.
PushPoint(clickPoint, candidatePoints);
if (currentPathLength > 0)
{
// TODO(mgsergio): Should I remove lastClickedPoint from clickablePoints
// as well?
RemovePointFromPull(lastClickedPoint, reachablePoints);
m_drawerDelegate->DrawGoldenPath(GetGoldenPathPoints());
}
m_drawerDelegate->ClearAllVisualizedPoints();
m_drawerDelegate->VisualizePoints(reachablePoints);
m_drawerDelegate->VisualizePoints({clickPoint});
break;
case ClickType::Remove: // TODO(mgsergio): Rename this case.
if (button == Qt::MouseButton::LeftButton) // RemovePoint
{
m_drawerDelegate->ClearAllVisualizedPoints();
m_drawerDelegate->ClearGoldenPath();
PopPoint();
if (m_goldenPath.empty())
{
m_drawerDelegate->VisualizePoints(m_pointsDelegate->GetAllJunctionPointsInViewport());
}
else
{
m_drawerDelegate->VisualizePoints(
GetReachablePoints(GetLastPoint(), GetGoldenPathPoints(), *m_pointsDelegate, 1 /* lookBackIndex */));
}
if (GetPointsCount() > 1)
m_drawerDelegate->DrawGoldenPath(GetGoldenPathPoints());
}
else if (button == Qt::MouseButton::RightButton)
{
CommitPath();
}
break;
case ClickType::Miss:
// TODO(mgsergio): This situation should be handled by checking candidatePoitns.empty() above.
// Not shure though if all cases are handled by that check.
return;
}
}
bool TrafficMode::StartBuildingPathChecks() const
{
CHECK(m_currentSegment, ("A segment should be selected before path building is started."));
if (m_buildingPath)
MYTHROW(TrafficModeError, ("Path building already in progress."));
if (m_currentSegment->HasGoldenPath())
{
auto const btn = QMessageBox::question(nullptr /* parent */, "Override warning",
"The selected segment already has a golden path. Do you want to override?");
if (btn == QMessageBox::No)
return false;
}
return true;
}
} // namespace openlr

View file

@ -0,0 +1,117 @@
#pragma once
#include "points_controller_delegate_base.hpp"
#include "segment_correspondence.hpp"
#include "traffic_drawer_delegate_base.hpp"
#include "openlr/decoded_path.hpp"
#include "indexer/data_source.hpp"
#include "base/exception.hpp"
#include <pugixml.hpp>
#include <memory>
#include <string>
#include <vector>
#include <QAbstractTableModel>
class QItemSelection;
class Selection;
DECLARE_EXCEPTION(TrafficModeError, RootException);
namespace openlr
{
namespace impl
{
/// This class denotes a "non-deterministic" feature point.
/// I.e. it is a set of all pairs <FeatureID, point index>
/// located at a specified coordinate.
/// Only one point at a time is considered active.
class RoadPointCandidate
{
public:
RoadPointCandidate(std::vector<openlr::FeaturePoint> const & points, m2::PointD const & coord);
void ActivateCommonPoint(RoadPointCandidate const & rpc);
openlr::FeaturePoint const & GetPoint() const;
m2::PointD const & GetCoordinate() const;
private:
static size_t const kInvalidId;
void SetActivePoint(FeatureID const & fid);
m2::PointD m_coord = m2::PointD::Zero();
std::vector<openlr::FeaturePoint> m_points;
size_t m_activePointIndex = kInvalidId;
};
} // namespace impl
/// This class is used to map sample ids to real data
/// and change sample evaluations.
class TrafficMode : public QAbstractTableModel
{
Q_OBJECT
public:
// TODO(mgsergio): Check we are on the right mwm. I.e. right mwm version and everything.
TrafficMode(std::string const & dataFileName, DataSource const & dataSource,
std::unique_ptr<TrafficDrawerDelegateBase> drawerDelegate,
std::unique_ptr<PointsControllerDelegateBase> pointsDelegate, QObject * parent = Q_NULLPTR);
bool SaveSampleAs(std::string const & fileName) const;
int rowCount(QModelIndex const & parent = QModelIndex()) const Q_DECL_OVERRIDE;
int columnCount(QModelIndex const & parent = QModelIndex()) const Q_DECL_OVERRIDE;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;
QVariant data(QModelIndex const & index, int role) const Q_DECL_OVERRIDE;
Qt::ItemFlags flags(QModelIndex const & index) const Q_DECL_OVERRIDE;
bool IsBuildingPath() const { return m_buildingPath; }
void GoldifyMatchedPath();
void StartBuildingPath();
void PushPoint(m2::PointD const & coord, std::vector<FeaturePoint> const & points);
void PopPoint();
void CommitPath();
void RollBackPath();
void IgnorePath();
size_t GetPointsCount() const;
m2::PointD const & GetPoint(size_t const index) const;
m2::PointD const & GetLastPoint() const;
std::vector<m2::PointD> GetGoldenPathPoints() const;
public slots:
void OnItemSelected(QItemSelection const & selected, QItemSelection const &);
void OnClick(m2::PointD const & clickPoint, Qt::MouseButton const button) { HandlePoint(clickPoint, button); }
signals:
void EditingStopped();
void SegmentSelected(int segmentId);
private:
void HandlePoint(m2::PointD clickPoint, Qt::MouseButton const button);
bool StartBuildingPathChecks() const;
DataSource const & m_dataSource;
std::vector<SegmentCorrespondence> m_segments;
// Non-owning pointer to an element of m_segments.
SegmentCorrespondence * m_currentSegment = nullptr;
std::unique_ptr<TrafficDrawerDelegateBase> m_drawerDelegate;
std::unique_ptr<PointsControllerDelegateBase> m_pointsDelegate;
bool m_buildingPath = false;
std::vector<impl::RoadPointCandidate> m_goldenPath;
// Clone this document and add things to its clone when saving sample.
pugi::xml_document m_template;
};
} // namespace openlr

View file

@ -0,0 +1,76 @@
#include "openlr/openlr_match_quality/openlr_assessment_tool/traffic_panel.hpp"
#include "openlr/openlr_match_quality/openlr_assessment_tool/traffic_mode.hpp"
#include <QtCore/QAbstractTableModel>
#include <QtWidgets/QBoxLayout>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QStyledItemDelegate>
#include <QtWidgets/QTableView>
namespace openlr
{
// ComboBoxDelegate --------------------------------------------------------------------------------
ComboBoxDelegate::ComboBoxDelegate(QObject * parent) : QStyledItemDelegate(parent) {}
QWidget * ComboBoxDelegate::createEditor(QWidget * parent, QStyleOptionViewItem const & option,
QModelIndex const & index) const
{
auto * editor = new QComboBox(parent);
editor->setFrame(false);
editor->setEditable(false);
editor->addItems({"Unevaluated", "Positive", "Negative", "RelPositive", "RelNegative", "Ignore"});
return editor;
}
void ComboBoxDelegate::setEditorData(QWidget * editor, QModelIndex const & index) const
{
auto const value = index.model()->data(index, Qt::EditRole).toString();
static_cast<QComboBox *>(editor)->setCurrentText(value);
}
void ComboBoxDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, QModelIndex const & index) const
{
model->setData(index, static_cast<QComboBox *>(editor)->currentText(), Qt::EditRole);
}
void ComboBoxDelegate::updateEditorGeometry(QWidget * editor, QStyleOptionViewItem const & option,
QModelIndex const & index) const
{
editor->setGeometry(option.rect);
}
// TrafficPanel ------------------------------------------------------------------------------------
TrafficPanel::TrafficPanel(QAbstractItemModel * trafficModel, QWidget * parent) : QWidget(parent)
{
CreateTable(trafficModel);
auto * layout = new QVBoxLayout();
layout->addWidget(m_table);
setLayout(layout);
// Select first segment by default;
auto const & index = m_table->model()->index(0, 0);
m_table->selectionModel()->select(index, QItemSelectionModel::Select);
}
void TrafficPanel::CreateTable(QAbstractItemModel * trafficModel)
{
m_table = new QTableView();
m_table->setFocusPolicy(Qt::NoFocus);
m_table->setAlternatingRowColors(true);
m_table->setShowGrid(false);
m_table->setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows);
m_table->setSelectionMode(QAbstractItemView::SelectionMode::SingleSelection);
m_table->verticalHeader()->setVisible(false);
m_table->horizontalHeader()->setVisible(true);
m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
m_table->setModel(trafficModel);
m_table->setItemDelegate(new ComboBoxDelegate());
connect(m_table->selectionModel(), SIGNAL(selectionChanged(QItemSelection const &, QItemSelection const &)),
trafficModel, SLOT(OnItemSelected(QItemSelection const &, QItemSelection const &)));
}
} // namespace openlr

View file

@ -0,0 +1,49 @@
#pragma once
#include <QtWidgets/QStyledItemDelegate>
class QAbstractItemModel;
class QComboBox;
class QTableView;
class QWidget;
namespace openlr
{
class ComboBoxDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
ComboBoxDelegate(QObject * parent = 0);
QWidget * createEditor(QWidget * parent, QStyleOptionViewItem const & option,
QModelIndex const & index) const Q_DECL_OVERRIDE;
void setEditorData(QWidget * editor, QModelIndex const & index) const Q_DECL_OVERRIDE;
void setModelData(QWidget * editor, QAbstractItemModel * model, QModelIndex const & index) const Q_DECL_OVERRIDE;
void updateEditorGeometry(QWidget * editor, QStyleOptionViewItem const & option,
QModelIndex const & index) const Q_DECL_OVERRIDE;
};
class TrafficPanel : public QWidget
{
Q_OBJECT
public:
explicit TrafficPanel(QAbstractItemModel * trafficModel, QWidget * parent);
private:
void CreateTable(QAbstractItemModel * trafficModel);
void FillTable();
signals:
public slots:
// void OnCheckBoxClicked(int row, int state);
private:
QTableView * m_table = Q_NULLPTR;
};
} // namespace openlr

View file

@ -0,0 +1,50 @@
#include "openlr/openlr_match_quality/openlr_assessment_tool/trafficmodeinitdlg.h"
#include "ui_trafficmodeinitdlg.h"
#include "platform/settings.hpp"
#include <QtWidgets/QFileDialog>
#include <string>
namespace
{
std::string const kDataFilePath = "LastOpenlrAssessmentDataFilePath";
} // namespace
namespace openlr
{
TrafficModeInitDlg::TrafficModeInitDlg(QWidget * parent) : QDialog(parent), m_ui(new Ui::TrafficModeInitDlg)
{
m_ui->setupUi(this);
std::string lastDataFilePath;
if (settings::Get(kDataFilePath, lastDataFilePath))
m_ui->dataFileName->setText(QString::fromStdString(lastDataFilePath));
connect(m_ui->chooseDataFileButton, &QPushButton::clicked,
[this](bool) { SetFilePathViaDialog(*m_ui->dataFileName, tr("Choose data file"), "*.xml"); });
}
TrafficModeInitDlg::~TrafficModeInitDlg()
{
delete m_ui;
}
void TrafficModeInitDlg::accept()
{
m_dataFileName = m_ui->dataFileName->text().trimmed().toStdString();
settings::Set(kDataFilePath, m_dataFileName);
QDialog::accept();
}
void TrafficModeInitDlg::SetFilePathViaDialog(QLineEdit & dest, QString const & title, QString const & filter)
{
QFileDialog openFileDlg(nullptr, title, {} /* directory */, filter);
openFileDlg.exec();
if (openFileDlg.result() != QDialog::DialogCode::Accepted)
return;
dest.setText(openFileDlg.selectedFiles().first());
}
} // namespace openlr

View file

@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <QtWidgets/QDialog>
class QLineEdit;
namespace Ui {
class TrafficModeInitDlg;
}
namespace openlr
{
class TrafficModeInitDlg : public QDialog
{
Q_OBJECT
public:
explicit TrafficModeInitDlg(QWidget * parent = nullptr);
~TrafficModeInitDlg();
std::string GetDataFilePath() const { return m_dataFileName; }
private:
void SetFilePathViaDialog(QLineEdit & dest, QString const & title,
QString const & filter = {});
public slots:
void accept() override;
private:
Ui::TrafficModeInitDlg * m_ui;
std::string m_dataFileName;
};
} // namespace openlr

View file

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TrafficModeInitDlg</class>
<widget class="QDialog" name="TrafficModeInitDlg">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>482</width>
<height>122</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Select traffic files</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>441</width>
<height>101</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="2">
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="chooseDataFileButton">
<property name="text">
<string>Choose...</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="okButton">
<property name="text">
<string>Ok</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QLineEdit" name="dataFileName"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="dataLabel">
<property name="mouseTracking">
<bool>true</bool>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Data file:&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<tabstops>
<tabstop>dataFileName</tabstop>
<tabstop>chooseDataFileButton</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>cancelButton</sender>
<signal>clicked()</signal>
<receiver>TrafficModeInitDlg</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>290</x>
<y>103</y>
</hint>
<hint type="destinationlabel">
<x>164</x>
<y>98</y>
</hint>
</hints>
</connection>
<connection>
<sender>okButton</sender>
<signal>clicked()</signal>
<receiver>TrafficModeInitDlg</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>405</x>
<y>105</y>
</hint>
<hint type="destinationlabel">
<x>59</x>
<y>94</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View file

@ -0,0 +1,41 @@
#include "openlr/openlr_model.hpp"
#include "geometry/mercator.hpp"
#include "base/assert.hpp"
using namespace std;
namespace openlr
{
// LinearSegment -----------------------------------------------------------------------------------
vector<m2::PointD> LinearSegment::GetMercatorPoints() const
{
vector<m2::PointD> points;
points.reserve(m_locationReference.m_points.size());
for (auto const & point : m_locationReference.m_points)
points.push_back(mercator::FromLatLon(point.m_latLon));
return points;
}
vector<LocationReferencePoint> const & LinearSegment::GetLRPs() const
{
return m_locationReference.m_points;
}
vector<LocationReferencePoint> & LinearSegment::GetLRPs()
{
return m_locationReference.m_points;
}
string DebugPrint(LinearSegmentSource source)
{
switch (source)
{
case LinearSegmentSource::NotValid: return "NotValid";
case LinearSegmentSource::FromLocationReferenceTag: return "FromLocationReferenceTag";
case LinearSegmentSource::FromCoordinatesTag: return "FromCoordinatesTag";
}
UNREACHABLE();
}
} // namespace openlr

View file

@ -0,0 +1,124 @@
#pragma once
#include "geometry/latlon.hpp"
#include "geometry/point2d.hpp"
#include <cstdint>
#include <limits>
#include <string>
#include <vector>
namespace openlr
{
// The form of way (FOW) describes the physical road type of a line.
enum class FormOfWay
{
// The physical road type is unknown.
Undefined,
// A road permitted for motorized vehicles only in combination with a prescribed minimum speed.
// It has two or more physically separated carriageways and no single level-crossings.
Motorway,
// A road with physically separated carriageways regardless of the number of lanes.
// If a road is also a motorway, it should be coded as such and not as a multiple carriageway.
MultipleCarriageway,
// All roads without separate carriageways are considered as roads with a single carriageway.
SingleCarriageway,
// A road which forms a ring on which traffic traveling in only one direction is allowed.
Roundabout,
// An open area (partly) enclosed by roads which is used for non-traffic purposes
// and which is not a Roundabout.
Trafficsquare,
// A road especially designed to enter or leave a line.
Sliproad,
// The physical road type is known, but does not fit into one of the other categories.
Other,
// A path only allowed for bikes.
BikePath,
// A path only allowed for pedestrians.
Footpath,
NotAValue
};
// The functional road class (FRC) of a line is a road classification based on the importance
// of the road represented by the line.
enum class FunctionalRoadClass
{
// Main road, highest importance.
FRC0,
// First class road.
FRC1,
// Other road classes.
FRC2,
FRC3,
FRC4,
FRC5,
FRC6,
FRC7,
NotAValue
};
// LinearSegment structure may be filled from olr:locationReference xml tag,
// from coordinates tag or not valid.
enum class LinearSegmentSource
{
NotValid,
FromLocationReferenceTag,
FromCoordinatesTag,
};
struct LocationReferencePoint
{
// Coordinates of the point of interest.
ms::LatLon m_latLon = ms::LatLon::Zero();
// The bearing (BEAR) describes the angle between the true North and a line which is defined
// by the coordinate of the LocationReferencePoint and a coordinate which is BEARDIST along
// the line defined by the LocationReference-point attributes.
// For more information see OpenLR-Whitepaper `Bearing' section.
uint8_t m_bearing = 0;
FunctionalRoadClass m_functionalRoadClass = FunctionalRoadClass::NotAValue;
FormOfWay m_formOfWay = FormOfWay::NotAValue;
// The distance to next point field describes the distance to the next LocationReferencePoint
// in the topological connection of the LocationReferencePoints. The distance is measured in meters
// and is calculated along the location reference path between two subsequent LR-points.
// The last LRP has the distance value 0.
// Should not be used in the last point of a segment.
uint32_t m_distanceToNextPoint = 0;
// The lowest FunctionalRoadClass to the next point (LFRCNP) is the lowest FunctionalRoadClass
// value which appears in the location reference path between two consecutive LR-points.
// This information could be used to limit the number of road classes which need to be
// scanned during the decoding.
// Should not be used in the last point of a segment.
FunctionalRoadClass m_lfrcnp = FunctionalRoadClass::NotAValue;
bool m_againstDrivingDirection = false;
};
struct LinearLocationReference
{
std::vector<LocationReferencePoint> m_points;
uint32_t m_positiveOffsetMeters = 0;
uint32_t m_negativeOffsetMeters = 0;
};
struct LinearSegment
{
static auto constexpr kInvalidSegmentId = std::numeric_limits<uint32_t>::max();
std::vector<m2::PointD> GetMercatorPoints() const;
std::vector<LocationReferencePoint> const & GetLRPs() const;
std::vector<LocationReferencePoint> & GetLRPs();
LinearSegmentSource m_source = LinearSegmentSource::NotValid;
// TODO(mgsergio): Think of using openlr::PartnerSegmentId
uint32_t m_segmentId = kInvalidSegmentId;
// TODO(mgsergio): Make sure that one segment cannot contain
// more than one location reference.
LinearLocationReference m_locationReference;
uint32_t m_segmentLengthMeters = 0;
// uint32_t m_segmentRefSpeed; Always null in partners data. (No docs found).
};
std::string DebugPrint(LinearSegmentSource source);
} // namespace openlr

View file

@ -0,0 +1,348 @@
#include "openlr/openlr_model_xml.hpp"
#include "openlr/openlr_model.hpp"
#include "geometry/mercator.hpp"
#include "base/logging.hpp"
#include <cstring>
#include <optional>
#include <type_traits>
#include <pugixml.hpp>
using namespace std;
namespace // Primitive utilities to handle simple OpenLR-like XML data.
{
template <typename Value>
bool IntegerFromXML(pugi::xml_node const & node, Value & value)
{
if (!node)
return false;
value = static_cast<Value>(is_signed<Value>::value ? node.text().as_int() : node.text().as_uint());
return true;
}
std::optional<double> DoubleFromXML(pugi::xml_node const & node)
{
return node ? node.text().as_double() : std::optional<double>();
}
bool GetLatLon(pugi::xml_node const & node, int32_t & lat, int32_t & lon)
{
if (!IntegerFromXML(node.child("olr:latitude"), lat) || !IntegerFromXML(node.child("olr:longitude"), lon))
return false;
return true;
}
// This helper is used to parse records like this:
// <olr:lfrcnp olr:table="olr001_FunctionalRoadClass" olr:code="4"/>
template <typename Value>
bool TableValueFromXML(pugi::xml_node const & node, Value & value)
{
if (!node)
return false;
value = static_cast<Value>(is_signed<Value>::value ? node.attribute("olr:code").as_int()
: node.attribute("olr:code").as_uint());
return true;
}
pugi::xml_node GetLinearLocationReference(pugi::xml_node const & node)
{
// There should be only one linear location reference child of a location reference.
return node.select_node(".//olr:locationReference/olr:optionLinearLocationReference").node();
}
pugi::xml_node GetCoordinates(pugi::xml_node const & node)
{
return node.select_node(".//coordinates").node();
}
bool IsLocationReferenceTag(pugi::xml_node const & node)
{
return node.select_node(".//olr:locationReference").node();
}
bool IsCoordinatesTag(pugi::xml_node const & node)
{
return node.select_node("coordinates").node();
}
// This helper is used do deal with xml nodes of the form
// <node>
// <value>integer<value>
// </node>
template <typename Value>
bool ValueFromXML(pugi::xml_node const & node, Value & value)
{
auto const valueNode = node.child("olr:value");
return IntegerFromXML(valueNode, value);
}
template <typename Value>
bool ParseValueIfExists(pugi::xml_node const & node, Value & value)
{
if (!node)
return true;
return ValueFromXML(node, value);
}
} // namespace
namespace // OpenLR tools and abstractions
{
bool FirstCoordinateFromXML(pugi::xml_node const & node, ms::LatLon & latLon)
{
int32_t lat, lon;
if (!GetLatLon(node.child("olr:coordinate"), lat, lon))
return false;
latLon.m_lat = ((lat - math::Sign(lat) * 0.5) * 360) / (1 << 24);
latLon.m_lon = ((lon - math::Sign(lon) * 0.5) * 360) / (1 << 24);
return true;
}
std::optional<ms::LatLon> LatLonFormXML(pugi::xml_node const & node)
{
auto const lat = DoubleFromXML(node.child("latitude"));
auto const lon = DoubleFromXML(node.child("longitude"));
return lat && lon ? ms::LatLon(*lat, *lon) : ms::LatLon::Zero();
}
bool CoordinateFromXML(pugi::xml_node const & node, ms::LatLon const & prevCoord, ms::LatLon & latLon)
{
int32_t lat, lon;
if (!GetLatLon(node.child("olr:coordinate"), lat, lon))
return false;
// This constant is provided by the given OpenLR variant
// with no special meaning and is used as a factor to store doubles as ints.
auto const kOpenlrDeltaFactor = 100000;
latLon.m_lat = prevCoord.m_lat + static_cast<double>(lat) / kOpenlrDeltaFactor;
latLon.m_lon = prevCoord.m_lon + static_cast<double>(lon) / kOpenlrDeltaFactor;
return true;
}
bool LinePropertiesFromXML(pugi::xml_node const & linePropNode, openlr::LocationReferencePoint & locPoint)
{
if (!linePropNode)
{
LOG(LERROR, ("linePropNode is NULL"));
return false;
}
if (!TableValueFromXML(linePropNode.child("olr:frc"), locPoint.m_functionalRoadClass))
{
LOG(LERROR, ("Can't parse functional road class"));
return false;
}
if (!TableValueFromXML(linePropNode.child("olr:fow"), locPoint.m_formOfWay))
{
LOG(LERROR, ("Can't parse form of a way"));
return false;
}
if (!ValueFromXML(linePropNode.child("olr:bearing"), locPoint.m_bearing))
{
LOG(LERROR, ("Can't parse bearing"));
return false;
}
return true;
}
bool PathPropertiesFromXML(pugi::xml_node const & locPointNode, openlr::LocationReferencePoint & locPoint)
{
// Last point does not contain path properties.
if (strcmp(locPointNode.name(), "olr:last") == 0)
return true;
auto const propNode = locPointNode.child("olr:pathProperties");
if (!propNode)
{
LOG(LERROR, ("Can't parse path properties"));
return false;
}
if (!ValueFromXML(propNode.child("olr:dnp"), locPoint.m_distanceToNextPoint))
{
LOG(LERROR, ("Can't parse dnp"));
return false;
}
if (!TableValueFromXML(propNode.child("olr:lfrcnp"), locPoint.m_lfrcnp))
{
LOG(LERROR, ("Can't parse lfrcnp"));
return false;
}
auto const directionNode = propNode.child("olr:againstDrivingDirection");
if (!directionNode)
{
LOG(LERROR, ("Can't parse driving direction"));
return false;
}
locPoint.m_againstDrivingDirection = directionNode.text().as_bool();
return true;
}
bool LocationReferencePointFromXML(pugi::xml_node const & locPointNode, openlr::LocationReferencePoint & locPoint)
{
if (!FirstCoordinateFromXML(locPointNode, locPoint.m_latLon))
{
LOG(LERROR, ("Can't get first coordinate"));
return false;
}
return LinePropertiesFromXML(locPointNode.child("olr:lineProperties"), locPoint) &&
PathPropertiesFromXML(locPointNode, locPoint);
}
bool LocationReferencePointFromXML(pugi::xml_node const & locPointNode, ms::LatLon const & firstPoint,
openlr::LocationReferencePoint & locPoint)
{
if (!CoordinateFromXML(locPointNode, firstPoint, locPoint.m_latLon))
{
LOG(LERROR, ("Can't get last coordinate"));
return false;
}
return LinePropertiesFromXML(locPointNode.child("olr:lineProperties"), locPoint) &&
PathPropertiesFromXML(locPointNode, locPoint);
}
bool LinearLocationReferenceFromXML(pugi::xml_node const & locRefNode, openlr::LinearLocationReference & locRef)
{
if (!locRefNode)
{
LOG(LERROR, ("Can't get location reference."));
return false;
}
{
openlr::LocationReferencePoint point;
if (!LocationReferencePointFromXML(locRefNode.child("olr:first"), point))
return false;
locRef.m_points.push_back(point);
}
for (auto const pointNode : locRefNode.select_nodes("olr:intermediates"))
{
openlr::LocationReferencePoint point;
if (!LocationReferencePointFromXML(pointNode.node(), locRef.m_points.back().m_latLon, point))
return false;
locRef.m_points.push_back(point);
}
{
openlr::LocationReferencePoint point;
if (!LocationReferencePointFromXML(locRefNode.child("olr:last"), locRef.m_points.back().m_latLon, point))
return false;
locRef.m_points.push_back(point);
}
if (!ParseValueIfExists(locRefNode.child("olr:positiveOffset"), locRef.m_positiveOffsetMeters))
{
LOG(LERROR, ("Can't parse positive offset"));
return false;
}
if (!ParseValueIfExists(locRefNode.child("olr:negativeOffset"), locRef.m_negativeOffsetMeters))
{
LOG(LERROR, ("Can't parse negative offset"));
return false;
}
return true;
}
bool CoordinatesFromXML(pugi::xml_node const & coordsNode, openlr::LinearLocationReference & locRef)
{
if (!coordsNode)
{
LOG(LERROR, ("Can't get <coordinates>."));
return false;
}
auto const latLonStart = LatLonFormXML(coordsNode.child("start"));
auto const latLonEnd = LatLonFormXML(coordsNode.child("end"));
if (!latLonStart || !latLonEnd)
{
LOG(LERROR, ("Can't get <start> or <end> of <coordinates>."));
return false;
}
LOG(LINFO, ("from:", *latLonStart, "to:", *latLonEnd));
locRef.m_points.resize(2);
locRef.m_points[0].m_latLon = *latLonStart;
locRef.m_points[1].m_latLon = *latLonEnd;
return true;
}
} // namespace
namespace openlr
{
bool ParseOpenlr(pugi::xml_document const & document, vector<LinearSegment> & segments)
{
for (auto const segmentXpathNode : document.select_nodes("//reportSegments"))
{
LinearSegment segment;
auto const & node = segmentXpathNode.node();
if (!IsLocationReferenceTag(node) && !IsCoordinatesTag(node))
{
LOG(LWARNING, ("A segment with a strange tag. It is not <coordinates>"
" or <optionLinearLocationReference>, skipping..."));
continue;
}
if (!SegmentFromXML(node, segment))
return false;
segments.push_back(segment);
}
return true;
}
bool SegmentFromXML(pugi::xml_node const & segmentNode, LinearSegment & segment)
{
CHECK(segmentNode, ());
if (!IntegerFromXML(segmentNode.child("ReportSegmentID"), segment.m_segmentId))
{
LOG(LERROR, ("Can't parse segment id"));
return false;
}
if (!IntegerFromXML(segmentNode.child("segmentLength"), segment.m_segmentLengthMeters))
{
LOG(LERROR, ("Can't parse segment length"));
return false;
}
if (IsLocationReferenceTag(segmentNode))
{
auto const locRefNode = GetLinearLocationReference(segmentNode);
auto const result = LinearLocationReferenceFromXML(locRefNode, segment.m_locationReference);
if (result)
segment.m_source = LinearSegmentSource::FromLocationReferenceTag;
return result;
}
CHECK(IsCoordinatesTag(segmentNode), ());
auto const coordsNode = GetCoordinates(segmentNode);
if (!CoordinatesFromXML(coordsNode, segment.m_locationReference))
return false;
CHECK_EQUAL(segment.m_locationReference.m_points.size(), 2, ());
segment.m_locationReference.m_points[0].m_distanceToNextPoint = segment.m_segmentLengthMeters;
segment.m_source = LinearSegmentSource::FromCoordinatesTag;
return true;
}
} // namespace openlr

View file

@ -0,0 +1,18 @@
#pragma once
#include <vector>
namespace pugi
{
class xml_document;
class xml_node;
} // namespace pugi
namespace openlr
{
struct LinearSegment;
bool SegmentFromXML(pugi::xml_node const & segmentNode, LinearSegment & segment);
bool ParseOpenlr(pugi::xml_document const & document, std::vector<LinearSegment> & segments);
} // namespace openlr

View file

@ -0,0 +1,12 @@
project(openlr_stat)
set(SRC openlr_stat.cpp)
omim_add_executable(${PROJECT_NAME} ${SRC})
target_link_libraries(${PROJECT_NAME}
openlr
storage
platform
gflags::gflags
)

View file

@ -0,0 +1,282 @@
#include "openlr/openlr_decoder.hpp"
#include "routing/road_graph.hpp"
#include "indexer/classificator_loader.hpp"
#include "indexer/data_source.hpp"
#include "storage/country_parent_getter.hpp"
#include "platform/local_country_file.hpp"
#include "platform/local_country_file_utils.hpp"
#include "platform/platform.hpp"
#include "openlr/decoded_path.hpp"
#include "openlr/openlr_model.hpp"
#include "openlr/openlr_model_xml.hpp"
#include "base/file_name_utils.hpp"
#include "base/stl_helpers.hpp"
#include <algorithm>
#include <clocale>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <unordered_map>
#include <vector>
#include <boost/regex.hpp>
#include <gflags/gflags.h>
#include <pugixml.hpp>
DEFINE_string(input, "", "Path to OpenLR file.");
DEFINE_string(spark_output, "", "Path to output file in spark-oriented format");
DEFINE_string(assessment_output, "", "Path to output file in assessment-tool oriented format");
DEFINE_string(non_matched_ids, "non-matched-ids.txt", "Path to a file ids of non-matched segments will be saved to");
DEFINE_string(mwms_path, "", "Path to a folder with mwms.");
DEFINE_string(resources_path, "", "Path to a folder with resources.");
DEFINE_int32(limit, -1, "Max number of segments to handle. -1 for all.");
DEFINE_bool(multipoints_only, false, "Only segments with multiple points to handle.");
DEFINE_int32(num_threads, 1, "Number of threads.");
DEFINE_string(ids_path, "", "Path to a file with segment ids to process.");
DEFINE_string(countries_filename, "",
"Name of countries file which describes mwm tree. Used to get country specific "
"routing restrictions.");
DEFINE_int32(algo_version, 0, "Use new decoding algorithm");
using namespace openlr;
namespace
{
int32_t const kMinNumThreads = 1;
int32_t const kMaxNumThreads = 128;
int32_t const kHandleAllSegments = -1;
void LoadDataSources(std::string const & pathToMWMFolder, std::vector<FrozenDataSource> & dataSources)
{
CHECK(Platform::IsDirectory(pathToMWMFolder), (pathToMWMFolder, "must be a directory."));
Platform::FilesList files;
Platform::GetFilesByRegExp(pathToMWMFolder, boost::regex(".*\\") + DATA_FILE_EXTENSION, files);
CHECK(!files.empty(), (pathToMWMFolder, "Contains no .mwm files."));
size_t const numDataSources = dataSources.size();
std::vector<uint64_t> numCountries(numDataSources);
for (auto const & fileName : files)
{
auto const fullFileName = base::JoinPath(pathToMWMFolder, fileName);
ModelReaderPtr reader(GetPlatform().GetReader(fullFileName, "f"));
platform::LocalCountryFile localFile(pathToMWMFolder, platform::CountryFile(base::FilenameWithoutExt(fileName)),
version::ReadVersionDate(reader));
LOG(LINFO, ("Found mwm:", fullFileName));
try
{
localFile.SyncWithDisk();
for (size_t i = 0; i < numDataSources; ++i)
{
auto const result = dataSources[i].RegisterMap(localFile);
CHECK_EQUAL(result.second, MwmSet::RegResult::Success, ("Can't register mwm:", localFile));
auto const & info = result.first.GetInfo();
if (info && info->GetType() == MwmInfo::COUNTRY)
++numCountries[i];
}
}
catch (RootException const & ex)
{
CHECK(false, (ex.Msg(), "Bad mwm file:", localFile));
}
}
for (size_t i = 0; i < numDataSources; ++i)
if (numCountries[i] == 0)
LOG(LWARNING, ("No countries for thread", i));
}
bool ValidateLimit(char const * flagname, int32_t value)
{
if (value < -1)
{
LOG(LINFO, ("Valid value for --", std::string(flagname), ":", value, "must be greater or equal to -1."));
return false;
}
return true;
}
bool ValidateNumThreads(char const * flagname, int32_t value)
{
if (value < kMinNumThreads || value > kMaxNumThreads)
{
LOG(LINFO, ("Valid value for --", std::string(flagname), ":", value, "must be between", kMinNumThreads, "and",
kMaxNumThreads));
return false;
}
return true;
}
bool ValidateMwmPath(char const * flagname, std::string const & value)
{
if (value.empty())
{
LOG(LINFO, ("--", std::string(flagname), "should be specified."));
return false;
}
return true;
}
bool ValidateVersion(char const * flagname, int32_t value)
{
if (value == 0)
{
LOG(LINFO, ("--", std::string(flagname), "should be specified."));
return false;
}
if (value != 1 && value != 2 && value != 3)
{
LOG(LINFO, ("--", std::string(flagname), "should be one of 1, 2 or 3."));
return false;
}
return true;
}
bool const g_limitDummy = gflags::RegisterFlagValidator(&FLAGS_limit, &ValidateLimit);
bool const g_numThreadsDummy = gflags::RegisterFlagValidator(&FLAGS_num_threads, &ValidateNumThreads);
bool const g_mwmsPathDummy = gflags::RegisterFlagValidator(&FLAGS_mwms_path, &ValidateMwmPath);
bool const g_algoVersion = gflags::RegisterFlagValidator(&FLAGS_algo_version, &ValidateVersion);
void SaveNonMatchedIds(std::string const & filename, std::vector<DecodedPath> const & paths)
{
if (filename.empty())
return;
std::ofstream ofs(filename);
for (auto const & p : paths)
if (p.m_path.empty())
ofs << p.m_segmentId << std::endl;
}
std::vector<LinearSegment> LoadSegments(pugi::xml_document & document)
{
std::vector<LinearSegment> segments;
if (!ParseOpenlr(document, segments))
{
LOG(LERROR, ("Can't parse data."));
exit(-1);
}
OpenLRDecoder::SegmentsFilter filter(FLAGS_ids_path, FLAGS_multipoints_only);
if (FLAGS_limit != kHandleAllSegments && FLAGS_limit >= 0 && static_cast<size_t>(FLAGS_limit) < segments.size())
segments.resize(FLAGS_limit);
base::EraseIf(segments, [&filter](LinearSegment const & segment) { return !filter.Matches(segment); });
std::sort(segments.begin(), segments.end(), base::LessBy(&LinearSegment::m_segmentId));
return segments;
}
void WriteAssessmentFile(std::string const fileName, pugi::xml_document const & doc,
std::vector<DecodedPath> const & paths)
{
std::unordered_map<uint32_t, pugi::xml_node> xmlSegments;
for (auto const & xpathNode : doc.select_nodes("//reportSegments"))
{
auto const xmlSegment = xpathNode.node();
xmlSegments[xmlSegment.child("ReportSegmentID").text().as_uint()] = xmlSegment;
}
pugi::xml_document result;
auto segments = result.append_child("Segments");
auto const dict = doc.select_node(".//Dictionary").node();
char const xmlns[] = "xmlns";
// Copy namespaces from <Dictionary> to <Segments>
for (auto const & attr : dict.attributes())
{
if (strncmp(xmlns, attr.name(), sizeof(xmlns) - 1) != 0)
continue;
// Don't copy default namespace.
if (strncmp(xmlns, attr.name(), sizeof(xmlns)) == 0)
continue;
segments.append_copy(attr);
}
for (auto const & p : paths)
{
auto segment = segments.append_child("Segment");
{
auto const xmlSegment = xmlSegments[p.m_segmentId.Get()];
segment.append_copy(xmlSegment);
}
if (!p.m_path.empty())
{
auto node = segment.append_child("Route");
openlr::PathToXML(p.m_path, node);
}
}
result.save_file(fileName.data(), " " /* indent */);
}
} // namespace
int main(int argc, char * argv[])
{
gflags::SetUsageMessage("OpenLR stats tool.");
gflags::ParseCommandLineFlags(&argc, &argv, true);
if (!FLAGS_resources_path.empty())
GetPlatform().SetResourceDir(FLAGS_resources_path);
classificator::Load();
auto const numThreads = static_cast<uint32_t>(FLAGS_num_threads);
std::vector<FrozenDataSource> dataSources(numThreads);
LoadDataSources(FLAGS_mwms_path, dataSources);
OpenLRDecoder decoder(dataSources,
storage::CountryParentGetter(FLAGS_countries_filename, GetPlatform().ResourcesDir()));
pugi::xml_document document;
auto const load_result = document.load_file(FLAGS_input.data());
if (!load_result)
{
LOG(LERROR, ("Can't load file", FLAGS_input, ":", load_result.description()));
exit(-1);
}
std::setlocale(LC_ALL, "en_US.UTF-8");
auto const segments = LoadSegments(document);
std::vector<DecodedPath> paths(segments.size());
switch (FLAGS_algo_version)
{
case 2: decoder.DecodeV2(segments, numThreads, paths); break;
case 3: decoder.DecodeV3(segments, numThreads, paths); break;
default: CHECK(false, ("Wrong algorithm version."));
}
SaveNonMatchedIds(FLAGS_non_matched_ids, paths);
if (!FLAGS_assessment_output.empty())
WriteAssessmentFile(FLAGS_assessment_output, document, paths);
if (!FLAGS_spark_output.empty())
WriteAsMappingForSpark(FLAGS_spark_output, paths);
return 0;
}

View file

@ -0,0 +1,12 @@
project(openlr_tests)
set(SRC decoded_path_test.cpp)
omim_add_test(${PROJECT_NAME} ${SRC})
target_link_libraries(${PROJECT_NAME}
generator_tests_support
platform_tests_support
openlr
indexer
)

View file

@ -0,0 +1,208 @@
#include "testing/testing.hpp"
#include "openlr/decoded_path.hpp"
#include "generator/generator_tests_support/test_feature.hpp"
#include "generator/generator_tests_support/test_mwm_builder.hpp"
#include "indexer/classificator_loader.hpp"
#include "indexer/data_source.hpp"
#include "platform/country_file.hpp"
#include "platform/local_country_file.hpp"
#include "platform/platform.hpp"
#include "platform/platform_tests_support/scoped_dir.hpp"
#include "platform/platform_tests_support/scoped_file.hpp"
#include "base/checked_cast.hpp"
#include "base/file_name_utils.hpp"
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <vector>
#include <pugixml.hpp>
using namespace generator::tests_support;
using namespace platform::tests_support;
using namespace platform;
using namespace std;
namespace
{
string const kTestDir = "openlr_decoded_path_test";
string const kTestMwm = "test";
double RoughUpToFive(double d)
{
stringstream s;
s << setprecision(5) << fixed;
s << d;
s >> d;
return d;
}
m2::PointD RoughPoint(m2::PointD const & p)
{
return {RoughUpToFive(p.x), RoughUpToFive(p.y)};
}
geometry::PointWithAltitude RoughJunction(geometry::PointWithAltitude const & j)
{
return geometry::PointWithAltitude(RoughPoint(j.GetPoint()), j.GetAltitude());
}
routing::Edge RoughEdgeJunctions(routing::Edge const & e)
{
return routing::Edge::MakeReal(e.GetFeatureId(), e.IsForward(), e.GetSegId(), RoughJunction(e.GetStartJunction()),
RoughJunction(e.GetEndJunction()));
}
void RoughJunctionsInPath(openlr::Path & p)
{
for (auto & e : p)
e = RoughEdgeJunctions(e);
}
void TestSerializeDeserialize(openlr::Path const & path, DataSource const & dataSource)
{
pugi::xml_document doc;
openlr::PathToXML(path, doc);
openlr::Path restoredPath;
openlr::PathFromXML(doc, dataSource, restoredPath);
// Fix mercator::From/ToLatLon floating point error
// for we could use TEST_EQUAL on result.
RoughJunctionsInPath(restoredPath);
TEST_EQUAL(path, restoredPath, ());
}
openlr::Path MakePath(FeatureType const & road, bool const forward)
{
CHECK_EQUAL(road.GetGeomType(), feature::GeomType::Line, ());
CHECK_GREATER(road.GetPointsCount(), 0, ());
openlr::Path path;
size_t const maxPointIndex = road.GetPointsCount() - 1;
for (size_t i = 0; i < maxPointIndex; ++i)
{
size_t current{};
size_t next{};
if (forward)
{
current = i;
next = i + 1;
}
else
{
current = maxPointIndex - i;
next = current - 1;
}
auto const from = road.GetPoint(current);
auto const to = road.GetPoint(next);
path.push_back(routing::Edge::MakeReal(
road.GetID(), forward, base::checked_cast<uint32_t>(current - static_cast<size_t>(!forward)) /* segId */,
geometry::PointWithAltitude(from, 0 /* altitude */), geometry::PointWithAltitude(to, 0 /* altitude */)));
}
RoughJunctionsInPath(path);
return path;
}
template <typename Func>
void WithRoad(vector<m2::PointD> const & points, Func && fn)
{
classificator::Load();
auto & platform = GetPlatform();
auto const mwmPath = base::JoinPath(platform.WritableDir(), kTestDir);
LocalCountryFile country(mwmPath, CountryFile(kTestMwm), 0 /* version */);
ScopedDir testScopedDir(kTestDir);
ScopedFile testScopedMwm(base::JoinPath(kTestDir, kTestMwm + DATA_FILE_EXTENSION), ScopedFile::Mode::Create);
{
TestMwmBuilder builder(country, feature::DataHeader::MapType::Country);
builder.Add(TestRoad(points, "Interstate 60", "en"));
}
FrozenDataSource dataSource;
auto const regResult = dataSource.RegisterMap(country);
TEST_EQUAL(regResult.second, MwmSet::RegResult::Success, ());
MwmSet::MwmHandle mwmHandle = dataSource.GetMwmHandleById(regResult.first);
TEST(mwmHandle.IsAlive(), ());
FeaturesLoaderGuard const guard(dataSource, regResult.first);
auto road = guard.GetFeatureByIndex(0);
TEST(road, ());
road->ParseGeometry(FeatureType::BEST_GEOMETRY);
fn(dataSource, *road);
}
UNIT_TEST(MakePath_Test)
{
std::vector<m2::PointD> const points{{0, 0}, {0, 1}, {1, 0}, {1, 1}};
WithRoad(points, [&points](DataSource const & dataSource, FeatureType & road)
{
auto const & id = road.GetID();
{
openlr::Path const expected{routing::Edge::MakeReal(id, true /* forward */, 0 /* segId*/, points[0], points[1]),
routing::Edge::MakeReal(id, true /* forward */, 1 /* segId*/, points[1], points[2]),
routing::Edge::MakeReal(id, true /* forward */, 2 /* segId*/, points[2], points[3])};
auto const path = MakePath(road, true /* forward */);
TEST_EQUAL(path, expected, ());
}
{
openlr::Path const expected{routing::Edge::MakeReal(id, false /* forward */, 2 /* segId*/, points[3], points[2]),
routing::Edge::MakeReal(id, false /* forward */, 1 /* segId*/, points[2], points[1]),
routing::Edge::MakeReal(id, false /* forward */, 0 /* segId*/, points[1], points[0])};
{
auto const path = MakePath(road, false /* forward */);
TEST_EQUAL(path, expected, ());
}
}
});
}
UNIT_TEST(PathSerializeDeserialize_Test)
{
WithRoad({{0, 0}, {0, 1}, {1, 0}, {1, 1}}, [](DataSource const & dataSource, FeatureType & road)
{
{
auto const path = MakePath(road, true /* forward */);
TestSerializeDeserialize(path, dataSource);
}
{
auto const path = MakePath(road, false /* forward */);
TestSerializeDeserialize(path, dataSource);
}
});
}
UNIT_TEST(GetPoints_Test)
{
vector<m2::PointD> const points{{0, 0}, {0, 1}, {1, 0}, {1, 1}};
WithRoad(points, [&points](DataSource const &, FeatureType & road)
{
{
auto path = MakePath(road, true /* forward */);
// RoughJunctionsInPath(path);
TEST_EQUAL(GetPoints(path), points, ());
}
{
auto path = MakePath(road, false /* forward */);
// RoughJunctionsInPath(path);
auto reversed = points;
reverse(begin(reversed), end(reversed));
TEST_EQUAL(GetPoints(path), reversed, ());
}
});
}
} // namespace

View file

@ -0,0 +1,219 @@
#include "openlr/paths_connector.hpp"
#include "openlr/helpers.hpp"
#include "base/checked_cast.hpp"
#include "base/stl_iterator.hpp"
#include <algorithm>
#include <map>
#include <queue>
#include <tuple>
using namespace std;
namespace openlr
{
namespace
{
bool ValidatePath(Graph::EdgeVector const & path, double const distanceToNextPoint, double const pathLengthTolerance)
{
double pathLen = 0.0;
for (auto const & e : path)
pathLen += EdgeLength(e);
double pathDiffPercent =
AbsDifference(static_cast<double>(distanceToNextPoint), pathLen) / static_cast<double>(distanceToNextPoint);
LOG(LDEBUG, ("Validating path:", LogAs2GisPath(path)));
if (pathDiffPercent > pathLengthTolerance)
{
LOG(LDEBUG, ("Shortest path doest not meet required length constraints, error:", pathDiffPercent));
return false;
}
return true;
}
} // namespace
PathsConnector::PathsConnector(double pathLengthTolerance, Graph & graph, RoadInfoGetter & infoGetter, v2::Stats & stat)
: m_pathLengthTolerance(pathLengthTolerance)
, m_graph(graph)
, m_infoGetter(infoGetter)
, m_stat(stat)
{}
bool PathsConnector::ConnectCandidates(vector<LocationReferencePoint> const & points,
vector<vector<Graph::EdgeVector>> const & lineCandidates,
vector<Graph::EdgeVector> & resultPath)
{
ASSERT(!points.empty(), ());
resultPath.resize(points.size() - 1);
// TODO(mgsergio): Discard last point on failure.
// TODO(mgsergio): Do not iterate more than kMaxRetries times.
// TODO(mgserigio): Make kMaxRetries depend on points number in the segment.
for (size_t i = 1; i < points.size(); ++i)
{
bool found = false;
auto const & point = points[i - 1];
auto const distanceToNextPoint = static_cast<double>(point.m_distanceToNextPoint);
auto const & fromCandidates = lineCandidates[i - 1];
auto const & toCandidates = lineCandidates[i];
auto & resultPathPart = resultPath[i - 1];
Graph::EdgeVector fakePath;
for (size_t fromInd = 0; fromInd < fromCandidates.size() && !found; ++fromInd)
{
for (size_t toInd = 0; toInd < toCandidates.size() && !found; ++toInd)
{
resultPathPart.clear();
found = ConnectAdjacentCandidateLines(fromCandidates[fromInd], toCandidates[toInd], point.m_lfrcnp,
distanceToNextPoint, resultPathPart);
if (!found)
continue;
found = ValidatePath(resultPathPart, distanceToNextPoint, m_pathLengthTolerance);
if (fakePath.empty() && found && (resultPathPart.front().IsFake() || resultPathPart.back().IsFake()))
{
fakePath = resultPathPart;
found = false;
}
}
}
if (!fakePath.empty() && !found)
{
found = true;
resultPathPart = fakePath;
}
if (!found)
{
LOG(LDEBUG, ("No shortest path found"));
++m_stat.m_noShortestPathFound;
resultPathPart.clear();
return false;
}
}
ASSERT_EQUAL(resultPath.size(), points.size() - 1, ());
return true;
}
bool PathsConnector::FindShortestPath(Graph::Edge const & from, Graph::Edge const & to,
FunctionalRoadClass lowestFrcToNextPoint, uint32_t maxPathLength,
Graph::EdgeVector & path)
{
// TODO(mgsergio): Turn Dijkstra to A*.
uint32_t const kLengthToleranceM = 10;
struct State
{
State(Graph::Edge const & e, uint32_t const s) : m_edge(e), m_score(s) {}
bool operator>(State const & o) const { return make_tuple(m_score, m_edge) > make_tuple(o.m_score, o.m_edge); }
Graph::Edge m_edge;
uint32_t m_score;
};
ASSERT(from.HasRealPart() && to.HasRealPart(), ());
priority_queue<State, vector<State>, greater<State>> q;
map<Graph::Edge, uint32_t> scores;
map<Graph::Edge, Graph::Edge> links;
q.emplace(from, 0);
scores[from] = 0;
while (!q.empty())
{
auto const state = q.top();
q.pop();
auto const & u = state.m_edge;
// TODO(mgsergio): Unify names: use either score or distance.
auto const us = state.m_score;
if (us > maxPathLength + kLengthToleranceM)
continue;
if (us > scores[u])
continue;
if (u == to)
{
for (auto e = u; e != from; e = links[e])
path.push_back(e);
path.push_back(from);
reverse(begin(path), end(path));
return true;
}
Graph::EdgeListT edges;
m_graph.GetOutgoingEdges(u.GetEndJunction(), edges);
for (auto const & e : edges)
{
if (!ConformLfrcnp(e, lowestFrcToNextPoint, 2 /* frcThreshold */, m_infoGetter))
continue;
// TODO(mgsergio): Use frc to filter edges.
// Only start and/or end of the route can be fake.
// Routes made only of fake edges are no used to us.
if (u.IsFake() && e.IsFake())
continue;
auto const it = scores.find(e);
auto const eScore = us + EdgeLength(e);
if (it == end(scores) || it->second > eScore)
{
scores[e] = eScore;
links[e] = u;
q.emplace(e, eScore);
}
}
}
return false;
}
bool PathsConnector::ConnectAdjacentCandidateLines(Graph::EdgeVector const & from, Graph::EdgeVector const & to,
FunctionalRoadClass lowestFrcToNextPoint, double distanceToNextPoint,
Graph::EdgeVector & resultPath)
{
ASSERT(!to.empty(), ());
if (auto const skip = PathOverlappingLen(from, to))
{
if (skip == -1)
return false;
copy(begin(from), end(from), back_inserter(resultPath));
copy(begin(to) + skip, end(to), back_inserter(resultPath));
return true;
}
ASSERT(from.back() != to.front(), ());
Graph::EdgeVector shortestPath;
auto const found = FindShortestPath(from.back(), to.front(), lowestFrcToNextPoint, distanceToNextPoint, shortestPath);
if (!found)
return false;
// Skip the last edge from |from| because it already took its place at begin(shortestPath).
copy(begin(from), prev(end(from)), back_inserter(resultPath));
copy(begin(shortestPath), end(shortestPath), back_inserter(resultPath));
// Skip the first edge from |to| because it already took its place at prev(end(shortestPath)).
copy(next(begin(to)), end(to), back_inserter(resultPath));
return found;
}
} // namespace openlr

View file

@ -0,0 +1,36 @@
#pragma once
#include "openlr/graph.hpp"
#include "openlr/openlr_model.hpp"
#include "openlr/road_info_getter.hpp"
#include "openlr/stats.hpp"
#include <cstddef>
#include <cstdint>
#include <vector>
namespace openlr
{
class PathsConnector
{
public:
PathsConnector(double pathLengthTolerance, Graph & graph, RoadInfoGetter & infoGetter, v2::Stats & stat);
bool ConnectCandidates(std::vector<LocationReferencePoint> const & points,
std::vector<std::vector<Graph::EdgeVector>> const & lineCandidates,
std::vector<Graph::EdgeVector> & resultPath);
private:
bool FindShortestPath(Graph::Edge const & from, Graph::Edge const & to, FunctionalRoadClass lowestFrcToNextPoint,
uint32_t maxPathLength, Graph::EdgeVector & path);
bool ConnectAdjacentCandidateLines(Graph::EdgeVector const & from, Graph::EdgeVector const & to,
FunctionalRoadClass lowestFrcToNextPoint, double distanceToNextPoint,
Graph::EdgeVector & resultPath);
double m_pathLengthTolerance;
Graph & m_graph;
RoadInfoGetter & m_infoGetter;
v2::Stats & m_stat;
};
} // namespace openlr

View file

@ -0,0 +1,37 @@
#include "openlr/road_info_getter.hpp"
#include "indexer/classificator.hpp"
#include "indexer/data_source.hpp"
#include "indexer/feature.hpp"
#include "base/assert.hpp"
namespace openlr
{
// RoadInfoGetter::RoadInfo ------------------------------------------------------------------------
RoadInfoGetter::RoadInfo::RoadInfo(FeatureType & ft)
: m_hwClass(ftypes::GetHighwayClass(feature::TypesHolder(ft)))
, m_link(ftypes::IsLinkChecker::Instance()(ft))
, m_oneWay(ftypes::IsOneWayChecker::Instance()(ft))
, m_isRoundabout(ftypes::IsRoundAboutChecker::Instance()(ft))
{}
// RoadInfoGetter ----------------------------------------------------------------------------------
RoadInfoGetter::RoadInfoGetter(DataSource const & dataSource) : m_dataSource(dataSource) {}
RoadInfoGetter::RoadInfo RoadInfoGetter::Get(FeatureID const & fid)
{
auto it = m_cache.find(fid);
if (it != end(m_cache))
return it->second;
FeaturesLoaderGuard g(m_dataSource, fid.m_mwmId);
auto ft = g.GetOriginalFeatureByIndex(fid.m_index);
CHECK(ft, ());
RoadInfo info(*ft);
it = m_cache.emplace(fid, info).first;
return it->second;
}
} // namespace openlr

View file

@ -0,0 +1,41 @@
#pragma once
#include "openlr/openlr_model.hpp"
#include "indexer/feature_data.hpp"
#include "indexer/ftypes_matcher.hpp"
#include <map>
class Classificator;
class DataSource;
namespace feature
{
class TypesHolder;
}
namespace openlr
{
class RoadInfoGetter final
{
public:
struct RoadInfo
{
explicit RoadInfo(FeatureType & ft);
ftypes::HighwayClass m_hwClass = ftypes::HighwayClass::Undefined;
bool m_link = false;
bool m_oneWay = false;
bool m_isRoundabout = false;
};
explicit RoadInfoGetter(DataSource const & dataSource);
RoadInfo Get(FeatureID const & fid);
private:
DataSource const & m_dataSource;
std::map<FeatureID, RoadInfo> m_cache;
};
} // namespace openlr

751
tools/openlr/router.cpp Normal file
View file

@ -0,0 +1,751 @@
#include "openlr/router.hpp"
#include "openlr/helpers.hpp"
#include "openlr/road_info_getter.hpp"
#include "routing/features_road_graph.hpp"
#include "platform/location.hpp"
#include "geometry/angles.hpp"
#include "geometry/mercator.hpp"
#include "geometry/segment2d.hpp"
#include "base/assert.hpp"
#include "base/math.hpp"
#include <algorithm>
#include <functional>
#include <limits>
#include <queue>
#include <utility>
#include <boost/iterator/transform_iterator.hpp>
using boost::make_transform_iterator;
namespace openlr
{
namespace
{
int const kFRCThreshold = 2;
size_t const kMaxRoadCandidates = 20;
double const kDistanceAccuracyM = 1000;
double const kEps = 1e-9;
double const kBearingDist = 25;
int const kNumBuckets = 256;
double const kAnglesInBucket = 360.0 / kNumBuckets;
uint32_t Bearing(m2::PointD const & a, m2::PointD const & b)
{
auto const angle = location::AngleToBearing(math::RadToDeg(ang::AngleTo(a, b)));
CHECK_LESS_OR_EQUAL(angle, 360, ("Angle should be less than or equal to 360."));
CHECK_GREATER_OR_EQUAL(angle, 0, ("Angle should be greater than or equal to 0"));
return math::Clamp(angle / kAnglesInBucket, 0.0, 255.0);
}
} // namespace
// Router::Vertex::Score ---------------------------------------------------------------------------
void Router::Vertex::Score::AddFakePenalty(double p, bool partOfReal)
{
m_penalty += (partOfReal ? kFakeCoeff : kTrueFakeCoeff) * p;
}
void Router::Vertex::Score::AddBearingPenalty(int expected, int actual)
{
ASSERT_LESS(expected, kNumBuckets, ());
ASSERT_GREATER_OR_EQUAL(expected, 0, ());
ASSERT_LESS(actual, kNumBuckets, ());
ASSERT_GREATER_OR_EQUAL(actual, 0, ());
int const diff = abs(expected - actual);
double angle = math::DegToRad(std::min(diff, kNumBuckets - diff) * kAnglesInBucket);
m_penalty += kBearingErrorCoeff * angle * kBearingDist;
}
bool Router::Vertex::Score::operator<(Score const & rhs) const
{
auto const ls = GetScore();
auto const rs = rhs.GetScore();
if (ls != rs)
return ls < rs;
if (m_distance != rhs.m_distance)
return m_distance < rhs.m_distance;
return m_penalty < rhs.m_penalty;
}
bool Router::Vertex::Score::operator==(Score const & rhs) const
{
return m_distance == rhs.m_distance && m_penalty == rhs.m_penalty;
}
// Router::Vertex ----------------------------------------------------------------------------------
Router::Vertex::Vertex(geometry::PointWithAltitude const & junction, geometry::PointWithAltitude const & stageStart,
double stageStartDistance, size_t stage, bool bearingChecked)
: m_junction(junction)
, m_stageStart(stageStart)
, m_stageStartDistance(stageStartDistance)
, m_stage(stage)
, m_bearingChecked(bearingChecked)
{}
bool Router::Vertex::operator<(Vertex const & rhs) const
{
if (m_junction != rhs.m_junction)
return m_junction < rhs.m_junction;
if (m_stageStart != rhs.m_stageStart)
return m_stageStart < rhs.m_stageStart;
if (m_stageStartDistance != rhs.m_stageStartDistance)
return m_stageStartDistance < rhs.m_stageStartDistance;
if (m_stage != rhs.m_stage)
return m_stage < rhs.m_stage;
return m_bearingChecked < rhs.m_bearingChecked;
}
bool Router::Vertex::operator==(Vertex const & rhs) const
{
return m_junction == rhs.m_junction && m_stageStart == rhs.m_stageStart &&
m_stageStartDistance == rhs.m_stageStartDistance && m_stage == rhs.m_stage &&
m_bearingChecked == rhs.m_bearingChecked;
}
// Router::Edge ------------------------------------------------------------------------------------
Router::Edge::Edge(Vertex const & u, Vertex const & v, routing::Edge const & raw, bool isSpecial)
: m_u(u)
, m_v(v)
, m_raw(raw)
, m_isSpecial(isSpecial)
{}
// static
Router::Edge Router::Edge::MakeNormal(Vertex const & u, Vertex const & v, routing::Edge const & raw)
{
return Edge(u, v, raw, false /* isSpecial */);
}
// static
Router::Edge Router::Edge::MakeSpecial(Vertex const & u, Vertex const & v)
{
return Edge(u, v, routing::Edge::MakeFake(u.m_junction, v.m_junction), true /* isSpecial */);
}
std::pair<m2::PointD, m2::PointD> Router::Edge::ToPair() const
{
auto const & e = m_raw;
return std::make_pair(e.GetStartJunction().GetPoint(), e.GetEndJunction().GetPoint());
}
std::pair<m2::PointD, m2::PointD> Router::Edge::ToPairRev() const
{
auto const & e = m_raw;
return std::make_pair(e.GetEndJunction().GetPoint(), e.GetStartJunction().GetPoint());
}
// Router::Router ----------------------------------------------------------------------------------
Router::Router(routing::FeaturesRoadGraph & graph, RoadInfoGetter & roadInfoGetter)
: m_graph(graph)
, m_roadInfoGetter(roadInfoGetter)
{}
bool Router::Go(std::vector<WayPoint> const & points, double positiveOffsetM, double negativeOffsetM, Path & path)
{
if (!Init(points, positiveOffsetM, negativeOffsetM))
return false;
return FindPath(path);
}
bool Router::Init(std::vector<WayPoint> const & points, double positiveOffsetM, double negativeOffsetM)
{
CHECK_GREATER_OR_EQUAL(points.size(), 2, ());
m_points = points;
m_positiveOffsetM = positiveOffsetM;
m_negativeOffsetM = negativeOffsetM;
m_graph.ResetFakes();
m_pivots.clear();
for (size_t i = 1; i + 1 < m_points.size(); ++i)
{
m_pivots.emplace_back();
auto & ps = m_pivots.back();
std::vector<std::pair<routing::Edge, geometry::PointWithAltitude>> vicinity;
m_graph.FindClosestEdges(
mercator::RectByCenterXYAndSizeInMeters(m_points[i].m_point, routing::FeaturesRoadGraph::kClosestEdgesRadiusM),
kMaxRoadCandidates, vicinity);
for (auto const & v : vicinity)
{
auto const & e = v.first;
ps.push_back(e.GetStartJunction().GetPoint());
ps.push_back(e.GetEndJunction().GetPoint());
}
if (ps.empty())
return false;
}
m_pivots.push_back({m_points.back().m_point});
CHECK_EQUAL(m_pivots.size() + 1, m_points.size(), ());
{
m_sourceJunction = geometry::PointWithAltitude(m_points.front().m_point, 0 /* altitude */);
std::vector<std::pair<routing::Edge, geometry::PointWithAltitude>> sourceVicinity;
m_graph.FindClosestEdges(mercator::RectByCenterXYAndSizeInMeters(m_sourceJunction.GetPoint(),
routing::FeaturesRoadGraph::kClosestEdgesRadiusM),
kMaxRoadCandidates, sourceVicinity);
m_graph.AddFakeEdges(m_sourceJunction, sourceVicinity);
}
{
m_targetJunction = geometry::PointWithAltitude(m_points.back().m_point, 0 /* altitude */);
std::vector<std::pair<routing::Edge, geometry::PointWithAltitude>> targetVicinity;
m_graph.FindClosestEdges(mercator::RectByCenterXYAndSizeInMeters(m_targetJunction.GetPoint(),
routing::FeaturesRoadGraph::kClosestEdgesRadiusM),
kMaxRoadCandidates, targetVicinity);
m_graph.AddFakeEdges(m_targetJunction, targetVicinity);
}
return true;
}
bool Router::FindPath(Path & path)
{
using State = std::pair<Vertex::Score, Vertex>;
std::priority_queue<State, std::vector<State>, std::greater<State>> queue;
std::map<Vertex, Vertex::Score> scores;
Links links;
auto const pushVertex =
[&queue, &scores, &links](Vertex const & u, Vertex const & v, Vertex::Score const & vertexScore, Edge const & e)
{
if ((scores.count(v) == 0 || scores[v].GetScore() > vertexScore.GetScore() + kEps) && u != v)
{
scores[v] = vertexScore;
links[v] = std::make_pair(u, e);
queue.emplace(vertexScore, v);
}
};
Vertex const s(m_sourceJunction, m_sourceJunction, 0 /* stageStartDistance */, 0 /* stage */,
false /* bearingChecked */);
CHECK(!NeedToCheckBearing(s, 0 /* distance */), ());
scores[s] = Vertex::Score();
queue.emplace(scores[s], s);
double const piS = GetPotential(s);
while (!queue.empty())
{
auto const p = queue.top();
queue.pop();
Vertex::Score const & su = p.first;
Vertex const & u = p.second;
if (su != scores[u])
continue;
if (IsFinalVertex(u))
{
std::vector<Edge> edges;
auto cur = u;
while (cur != s)
{
auto const & p = links[cur];
edges.push_back(p.second);
cur = p.first;
}
reverse(edges.begin(), edges.end());
return ReconstructPath(edges, path);
}
size_t const stage = u.m_stage;
double const distanceToNextPointM = m_points[stage].m_distanceToNextPointM;
double const piU = GetPotential(u);
double const ud = su.GetDistance() + piS - piU; // real distance to u
CHECK_LESS(stage, m_pivots.size(), ());
// max(kDistanceAccuracyM, m_distanceToNextPointM) is added here
// to throw out quite long paths.
if (ud > u.m_stageStartDistance + distanceToNextPointM + std::max(kDistanceAccuracyM, distanceToNextPointM))
continue;
if (NearNextStage(u, piU) && !u.m_bearingChecked)
{
Vertex v = u;
Vertex::Score sv = su;
if (u.m_junction != u.m_stageStart)
{
int const expected = m_points[stage].m_bearing;
int const actual = Bearing(u.m_stageStart.GetPoint(), u.m_junction.GetPoint());
sv.AddBearingPenalty(expected, actual);
}
v.m_bearingChecked = true;
pushVertex(u, v, sv, Edge::MakeSpecial(u, v));
}
if (MayMoveToNextStage(u, piU))
{
Vertex v(u.m_junction, u.m_junction, ud /* stageStartDistance */, stage + 1, false /* bearingChecked */);
double const piV = GetPotential(v);
Vertex::Score sv = su;
sv.AddDistance(std::max(piV - piU, 0.0));
sv.AddIntermediateErrorPenalty(mercator::DistanceOnEarth(v.m_junction.GetPoint(), m_points[v.m_stage].m_point));
if (IsFinalVertex(v))
{
int const expected = m_points.back().m_bearing;
int const actual = GetReverseBearing(u, links);
sv.AddBearingPenalty(expected, actual);
}
pushVertex(u, v, sv, Edge::MakeSpecial(u, v));
}
ForEachEdge(u, true /* outgoing */, m_points[stage].m_lfrcnp, [&](routing::Edge const & edge)
{
Vertex v = u;
v.m_junction = edge.GetEndJunction();
double const piV = GetPotential(v);
Vertex::Score sv = su;
double const w = GetWeight(edge);
sv.AddDistance(std::max(w + piV - piU, 0.0));
double const vd = ud + w; // real distance to v
if (NeedToCheckBearing(v, vd))
{
CHECK(!NeedToCheckBearing(u, ud), ());
double const delta = vd - v.m_stageStartDistance - kBearingDist;
auto const p = PointAtSegment(edge.GetStartJunction().GetPoint(), edge.GetEndJunction().GetPoint(), delta);
if (v.m_stageStart.GetPoint() != p)
{
int const expected = m_points[stage].m_bearing;
int const actual = Bearing(v.m_stageStart.GetPoint(), p);
sv.AddBearingPenalty(expected, actual);
}
v.m_bearingChecked = true;
}
if (vd > v.m_stageStartDistance + distanceToNextPointM)
sv.AddDistanceErrorPenalty(std::min(vd - v.m_stageStartDistance - distanceToNextPointM, w));
if (edge.IsFake())
sv.AddFakePenalty(w, edge.HasRealPart());
pushVertex(u, v, sv, Edge::MakeNormal(u, v, edge));
});
}
return false;
}
bool Router::NeedToCheckBearing(Vertex const & u, double distanceM) const
{
if (IsFinalVertex(u) || u.m_bearingChecked)
return false;
return distanceM >= u.m_stageStartDistance + kBearingDist;
}
double Router::GetPotential(Vertex const & u) const
{
if (IsFinalVertex(u))
return 0.0;
CHECK_LESS(u.m_stage, m_pivots.size(), ());
auto const & pivots = m_pivots[u.m_stage];
CHECK(!pivots.empty(), ("Empty list of pivots"));
double potential = std::numeric_limits<double>::max();
auto const & point = u.m_junction.GetPoint();
for (auto const & pivot : pivots)
potential = std::min(potential, mercator::DistanceOnEarth(pivot, point));
return potential;
}
bool Router::NearNextStage(Vertex const & u, double pi) const
{
return u.m_stage < m_pivots.size() && pi < kEps;
}
bool Router::MayMoveToNextStage(Vertex const & u, double pi) const
{
return NearNextStage(u, pi) && u.m_bearingChecked;
}
uint32_t Router::GetReverseBearing(Vertex const & u, Links const & links) const
{
m2::PointD const a = u.m_junction.GetPoint();
m2::PointD b = m2::PointD::Zero();
Vertex curr = u;
double passed = 0;
bool found = false;
while (true)
{
auto const it = links.find(curr);
if (it == links.end())
break;
auto const & p = it->second;
auto const & prev = p.first;
auto const & edge = p.second.m_raw;
if (prev.m_stage != curr.m_stage)
break;
double const weight = GetWeight(edge);
if (passed + weight >= kBearingDist)
{
double const delta = kBearingDist - passed;
b = PointAtSegment(edge.GetEndJunction().GetPoint(), edge.GetStartJunction().GetPoint(), delta);
found = true;
break;
}
passed += weight;
curr = prev;
}
if (!found)
b = curr.m_junction.GetPoint();
return Bearing(a, b);
}
template <typename Fn>
void Router::ForEachEdge(Vertex const & u, bool outgoing, FunctionalRoadClass restriction, Fn && fn)
{
routing::IRoadGraph::EdgeListT edges;
if (outgoing)
GetOutgoingEdges(u.m_junction, edges);
else
GetIngoingEdges(u.m_junction, edges);
for (auto const & edge : edges)
{
if (!ConformLfrcnp(edge, restriction, kFRCThreshold, m_roadInfoGetter))
continue;
fn(edge);
}
}
void Router::GetOutgoingEdges(geometry::PointWithAltitude const & u, routing::IRoadGraph::EdgeListT & edges)
{
GetEdges(u, &routing::IRoadGraph::GetRegularOutgoingEdges, &routing::IRoadGraph::GetFakeOutgoingEdges,
m_outgoingCache, edges);
}
void Router::GetIngoingEdges(geometry::PointWithAltitude const & u, routing::IRoadGraph::EdgeListT & edges)
{
GetEdges(u, &routing::IRoadGraph::GetRegularIngoingEdges, &routing::IRoadGraph::GetFakeIngoingEdges, m_ingoingCache,
edges);
}
void Router::GetEdges(geometry::PointWithAltitude const & u, RoadGraphEdgesGetter getRegular,
RoadGraphEdgesGetter getFake, EdgeCacheT & cache, routing::IRoadGraph::EdgeListT & edges)
{
auto const it = cache.find(u);
if (it == cache.end())
{
auto & es = cache[u];
(m_graph.*getRegular)(u, es);
edges.append(es.begin(), es.end());
}
else
{
auto const & es = it->second;
edges.append(es.begin(), es.end());
}
(m_graph.*getFake)(u, edges);
}
template <typename Fn>
void Router::ForEachNonFakeEdge(Vertex const & u, bool outgoing, FunctionalRoadClass restriction, Fn && fn)
{
ForEachEdge(u, outgoing, restriction, [&fn](routing::Edge const & edge)
{
if (!edge.IsFake())
fn(edge);
});
}
template <typename Fn>
void Router::ForEachNonFakeClosestEdge(Vertex const & u, FunctionalRoadClass const restriction, Fn && fn)
{
std::vector<std::pair<routing::Edge, geometry::PointWithAltitude>> vicinity;
m_graph.FindClosestEdges(mercator::RectByCenterXYAndSizeInMeters(u.m_junction.GetPoint(),
routing::FeaturesRoadGraph::kClosestEdgesRadiusM),
kMaxRoadCandidates, vicinity);
for (auto const & p : vicinity)
{
auto const & edge = p.first;
if (edge.IsFake())
continue;
if (!ConformLfrcnp(edge, restriction, kFRCThreshold, m_roadInfoGetter))
continue;
fn(edge);
}
}
template <typename It>
size_t Router::FindPrefixLengthToConsume(It b, It const e, double lengthM)
{
size_t n = 0;
while (b != e && lengthM > 0.0)
{
// Need p here to prolongate lifetime of (*b) if iterator
// dereferencing returns a temprorary object instead of a
// reference.
auto const & p = *b;
auto const & u = p.first;
auto const & v = p.second;
double const len = mercator::DistanceOnEarth(u, v);
if (2 * lengthM < len)
break;
lengthM -= len;
++n;
++b;
}
return n;
}
template <typename It>
double Router::GetCoverage(m2::PointD const & u, m2::PointD const & v, It b, It e)
{
double const kEps = 1e-5;
double const kLengthThresholdM = 1;
m2::PointD const uv = v - u;
double const sqlen = u.SquaredLength(v);
if (mercator::DistanceOnEarth(u, v) < kLengthThresholdM)
return 0;
std::vector<std::pair<double, double>> covs;
for (; b != e; ++b)
{
auto const s = b->m_u.m_junction.GetPoint();
auto const t = b->m_v.m_junction.GetPoint();
if (!m2::IsPointOnSegmentEps(s, u, v, kEps) || !m2::IsPointOnSegmentEps(t, u, v, kEps))
continue;
if (DotProduct(uv, t - s) < -kEps)
continue;
double const sp = DotProduct(uv, s - u) / sqlen;
double const tp = DotProduct(uv, t - u) / sqlen;
double const start = math::Clamp(std::min(sp, tp), 0.0, 1.0);
double const finish = math::Clamp(std::max(sp, tp), 0.0, 1.0);
covs.emplace_back(start, finish);
}
sort(covs.begin(), covs.end());
double coverage = 0;
size_t i = 0;
while (i != covs.size())
{
size_t j = i;
double const first = covs[i].first;
double last = covs[i].second;
while (j != covs.size() && covs[j].first <= last)
{
last = std::max(last, covs[j].second);
++j;
}
coverage += last - first;
i = j;
}
CHECK_LESS_OR_EQUAL(coverage, 1.0 + kEps, ());
return coverage;
}
template <typename It>
double Router::GetMatchingScore(m2::PointD const & u, m2::PointD const & v, It b, It e)
{
double const kEps = 1e-5;
double const len = mercator::DistanceOnEarth(u, v);
m2::PointD const uv = v - u;
double cov = 0;
for (; b != e; ++b)
{
// Need p here to prolongate lifetime of (*b) if iterator
// dereferencing returns a temprorary object instead of a
// reference.
auto const & p = *b;
auto const & s = p.first;
auto const & t = p.second;
if (!m2::IsPointOnSegmentEps(s, u, v, kEps) || !m2::IsPointOnSegmentEps(t, u, v, kEps))
break;
m2::PointD const st = t - s;
if (DotProduct(uv, st) < -kEps)
break;
cov += mercator::DistanceOnEarth(s, t);
}
return len == 0 ? 0 : math::Clamp(cov / len, 0.0, 1.0);
}
template <typename It, typename Fn>
void Router::ForStagePrefix(It b, It e, size_t stage, Fn && fn)
{
while (b != e && b->m_raw.IsFake() && b->m_u.m_stage == stage && b->m_v.m_stage == stage)
++b;
if (b != e && !b->m_raw.IsFake())
fn(b);
}
bool Router::ReconstructPath(std::vector<Edge> & edges, Path & path)
{
CHECK_GREATER_OR_EQUAL(m_points.size(), 2, ());
using EdgeIt = std::vector<Edge>::iterator;
using EdgeItRev = std::vector<Edge>::reverse_iterator;
double const kFakeCoverageThreshold = 0.5;
base::EraseIf(edges, [](auto && e) { return e.IsSpecial(); });
{
auto toPair = [](auto && e) { return e.ToPair(); };
size_t const n = FindPrefixLengthToConsume(make_transform_iterator(edges.begin(), toPair),
make_transform_iterator(edges.end(), toPair), m_positiveOffsetM);
CHECK_LESS_OR_EQUAL(n, edges.size(), ());
edges.erase(edges.begin(), edges.begin() + n);
}
{
auto toPairRev = [](auto && e) { return e.ToPairRev(); };
size_t const n = FindPrefixLengthToConsume(make_transform_iterator(edges.rbegin(), toPairRev),
make_transform_iterator(edges.rend(), toPairRev), m_negativeOffsetM);
CHECK_LESS_OR_EQUAL(n, edges.size(), ());
edges.erase(edges.begin() + edges.size() - n, edges.end());
}
double frontEdgeScore = -1.0;
routing::Edge frontEdge;
ForStagePrefix(edges.begin(), edges.end(), 0, [&](EdgeIt e)
{
ForEachNonFakeEdge(e->m_u, false /* outgoing */, m_points[0].m_lfrcnp, [&](routing::Edge const & edge)
{
auto toPairRev = [](auto && e) { return e.ToPairRev(); };
double const score = GetMatchingScore(edge.GetEndJunction().GetPoint(), edge.GetStartJunction().GetPoint(),
make_transform_iterator(EdgeItRev(e), toPairRev),
make_transform_iterator(edges.rend(), toPairRev));
if (score > frontEdgeScore)
{
frontEdgeScore = score;
frontEdge = edge.GetReverseEdge();
}
});
});
double backEdgeScore = -1.0;
routing::Edge backEdge;
ForStagePrefix(edges.rbegin(), edges.rend(), m_points.size() - 2 /* stage */, [&](EdgeItRev e)
{
ForEachNonFakeEdge(e->m_v, true /* outgoing */, m_points[m_points.size() - 2].m_lfrcnp,
[&](routing::Edge const & edge)
{
auto toPair = [](auto && e) { return e.ToPair(); };
double const score =
GetMatchingScore(edge.GetStartJunction().GetPoint(), edge.GetEndJunction().GetPoint(),
make_transform_iterator(e.base(), toPair), make_transform_iterator(edges.end(), toPair));
if (score > backEdgeScore)
{
backEdgeScore = score;
backEdge = edge;
}
});
});
path.clear();
for (auto const & e : edges)
if (!e.IsFake())
path.push_back(e.m_raw);
if (frontEdgeScore >= kFakeCoverageThreshold && !path.empty() && path.front() != frontEdge)
path.insert(path.begin(), frontEdge);
if (backEdgeScore >= kFakeCoverageThreshold && !path.empty() && path.back() != backEdge)
path.insert(path.end(), backEdge);
if (path.empty())
{
// This is the case for routes from fake edges only.
FindSingleEdgeApproximation(edges, path);
}
return !path.empty();
}
void Router::FindSingleEdgeApproximation(std::vector<Edge> const & edges, Path & path)
{
double const kCoverageThreshold = 0.5;
CHECK(all_of(edges.begin(), edges.end(), [](auto && e) { return e.IsFake(); }), ());
double expectedLength = 0;
for (auto const & edge : edges)
expectedLength += GetWeight(edge);
if (expectedLength < kEps)
return;
double bestCoverage = -1.0;
routing::Edge bestEdge;
auto checkEdge = [&](routing::Edge const & edge)
{
double const weight = GetWeight(edge);
double const fraction =
GetCoverage(edge.GetStartJunction().GetPoint(), edge.GetEndJunction().GetPoint(), edges.begin(), edges.end());
double const coverage = weight * fraction;
if (coverage >= bestCoverage)
{
bestCoverage = coverage;
bestEdge = edge;
}
};
for (auto const & edge : edges)
{
auto const & u = edge.m_u;
auto const & v = edge.m_v;
CHECK_EQUAL(u.m_stage, v.m_stage, ());
auto const stage = u.m_stage;
ForEachNonFakeClosestEdge(u, m_points[stage].m_lfrcnp, checkEdge);
ForEachNonFakeClosestEdge(v, m_points[stage].m_lfrcnp, checkEdge);
}
if (bestCoverage >= expectedLength * kCoverageThreshold)
path = {bestEdge};
}
} // namespace openlr

222
tools/openlr/router.hpp Normal file
View file

@ -0,0 +1,222 @@
#pragma once
#include "openlr/decoded_path.hpp"
#include "openlr/way_point.hpp"
#include "routing/road_graph.hpp"
#include "geometry/point2d.hpp"
#include <map>
#include <sstream>
#include <utility>
#include <vector>
namespace routing
{
class FeaturesRoadGraph;
}
namespace openlr
{
class RoadInfoGetter;
class Router final
{
public:
Router(routing::FeaturesRoadGraph & graph, RoadInfoGetter & roadInfoGetter);
bool Go(std::vector<WayPoint> const & points, double positiveOffsetM, double negativeOffsetM, Path & path);
private:
struct Vertex final
{
class Score final
{
public:
// A weight for total length of true fake edges.
static int const kTrueFakeCoeff = 10;
// A weight for total length of fake edges that are parts of some
// real edges.
static constexpr double kFakeCoeff = 0.001;
// A weight for passing too far from pivot points.
static int const kIntermediateErrorCoeff = 3;
// A weight for excess of distance limit.
static int const kDistanceErrorCoeff = 3;
// A weight for deviation from bearing.
static int const kBearingErrorCoeff = 5;
void AddDistance(double p) { m_distance += p; }
void AddFakePenalty(double p, bool partOfReal);
void AddIntermediateErrorPenalty(double p) { m_penalty += kIntermediateErrorCoeff * p; }
void AddDistanceErrorPenalty(double p) { m_penalty += kDistanceErrorCoeff * p; }
void AddBearingPenalty(int expected, int actual);
double GetDistance() const { return m_distance; }
double GetPenalty() const { return m_penalty; }
double GetScore() const { return m_distance + m_penalty; }
bool operator<(Score const & rhs) const;
bool operator>(Score const & rhs) const { return rhs < *this; }
bool operator==(Score const & rhs) const;
bool operator!=(Score const & rhs) const { return !(*this == rhs); }
private:
// Reduced length of path in meters.
double m_distance = 0.0;
double m_penalty = 0.0;
};
Vertex() = default;
Vertex(geometry::PointWithAltitude const & junction, geometry::PointWithAltitude const & stageStart,
double stageStartDistance, size_t stage, bool bearingChecked);
bool operator<(Vertex const & rhs) const;
bool operator==(Vertex const & rhs) const;
bool operator!=(Vertex const & rhs) const { return !(*this == rhs); }
m2::PointD GetPoint() const { return m_junction.GetPoint(); }
geometry::PointWithAltitude m_junction;
geometry::PointWithAltitude m_stageStart;
double m_stageStartDistance = 0.0;
size_t m_stage = 0;
bool m_bearingChecked = false;
};
friend std::string DebugPrint(Vertex const & u)
{
std::ostringstream os;
os << "Vertex [ ";
os << "junction: " << DebugPrint(u.m_junction) << ", ";
os << "stageStart: " << DebugPrint(u.m_stageStart) << ", ";
os << "stageStartDistance: " << u.m_stageStartDistance << ", ";
os << "stage: " << u.m_stage << ", ";
os << "bearingChecked: " << u.m_bearingChecked;
os << " ]";
return os.str();
}
struct Edge final
{
Edge() = default;
Edge(Vertex const & u, Vertex const & v, routing::Edge const & raw, bool isSpecial);
static Edge MakeNormal(Vertex const & u, Vertex const & v, routing::Edge const & raw);
static Edge MakeSpecial(Vertex const & u, Vertex const & v);
bool IsFake() const { return m_raw.IsFake(); }
bool IsSpecial() const { return m_isSpecial; }
std::pair<m2::PointD, m2::PointD> ToPair() const;
std::pair<m2::PointD, m2::PointD> ToPairRev() const;
Vertex m_u;
Vertex m_v;
routing::Edge m_raw;
bool m_isSpecial = false;
};
friend std::string DebugPrint(Edge const & edge)
{
std::ostringstream os;
os << "Edge [ ";
os << "u: " << DebugPrint(edge.m_u) << ", ";
os << "v: " << DebugPrint(edge.m_v) << ", ";
os << "raw: " << DebugPrint(edge.m_raw) << ", ";
os << "isSpecial: " << edge.m_isSpecial;
os << " ]";
return os.str();
}
using Links = std::map<Vertex, std::pair<Vertex, Edge>>;
using RoadGraphEdgesGetter = void (routing::IRoadGraph::*)(geometry::PointWithAltitude const & junction,
routing::IRoadGraph::EdgeListT & edges) const;
bool Init(std::vector<WayPoint> const & points, double positiveOffsetM, double negativeOffsetM);
bool FindPath(Path & path);
// Returns true if the bearing should be checked for |u|, if the
// real passed distance from the source vertex is |distanceM|.
bool NeedToCheckBearing(Vertex const & u, double distanceM) const;
double GetPotential(Vertex const & u) const;
// Returns true if |u| is located near portal to the next stage.
// |pi| is the potential of |u|.
bool NearNextStage(Vertex const & u, double pi) const;
// Returns true if it's possible to move to the next stage from |u|.
// |pi| is the potential of |u|.
bool MayMoveToNextStage(Vertex const & u, double pi) const;
// Returns true if |u| is a final vertex and the router may stop now.
bool IsFinalVertex(Vertex const & u) const { return u.m_stage == m_pivots.size(); }
double GetWeight(routing::Edge const & e) const
{
return mercator::DistanceOnEarth(e.GetStartJunction().GetPoint(), e.GetEndJunction().GetPoint());
}
double GetWeight(Edge const & e) const { return GetWeight(e.m_raw); }
uint32_t GetReverseBearing(Vertex const & u, Links const & links) const;
template <typename Fn>
void ForEachEdge(Vertex const & u, bool outgoing, FunctionalRoadClass restriction, Fn && fn);
void GetOutgoingEdges(geometry::PointWithAltitude const & u, routing::IRoadGraph::EdgeListT & edges);
void GetIngoingEdges(geometry::PointWithAltitude const & u, routing::IRoadGraph::EdgeListT & edges);
using EdgeCacheT = std::map<geometry::PointWithAltitude, routing::IRoadGraph::EdgeListT>;
void GetEdges(geometry::PointWithAltitude const & u, RoadGraphEdgesGetter getRegular, RoadGraphEdgesGetter getFake,
EdgeCacheT & cache, routing::IRoadGraph::EdgeListT & edges);
template <typename Fn>
void ForEachNonFakeEdge(Vertex const & u, bool outgoing, FunctionalRoadClass restriction, Fn && fn);
template <typename Fn>
void ForEachNonFakeClosestEdge(Vertex const & u, FunctionalRoadClass const restriction, Fn && fn);
template <typename It>
size_t FindPrefixLengthToConsume(It b, It const e, double lengthM);
// Finds all edges that are on (u, v) and have the same direction as
// (u, v). Then, computes the fraction of the union of these edges
// to the total length of (u, v).
template <typename It>
double GetCoverage(m2::PointD const & u, m2::PointD const & v, It b, It e);
// Finds the longest prefix of [b, e) that covers edge (u, v).
// Returns the fraction of the coverage to the length of the (u, v).
template <typename It>
double GetMatchingScore(m2::PointD const & u, m2::PointD const & v, It b, It e);
// Finds the longest prefix of fake edges of [b, e) that have the
// same stage as |stage|. If the prefix exists, passes its bounding
// iterator to |fn|.
template <typename It, typename Fn>
void ForStagePrefix(It b, It e, size_t stage, Fn && fn);
bool ReconstructPath(std::vector<Edge> & edges, Path & path);
void FindSingleEdgeApproximation(std::vector<Edge> const & edges, Path & path);
routing::FeaturesRoadGraph & m_graph;
EdgeCacheT m_outgoingCache, m_ingoingCache;
RoadInfoGetter & m_roadInfoGetter;
std::vector<WayPoint> m_points;
double m_positiveOffsetM = 0.0;
double m_negativeOffsetM = 0.0;
std::vector<std::vector<m2::PointD>> m_pivots;
geometry::PointWithAltitude m_sourceJunction;
geometry::PointWithAltitude m_targetJunction;
};
} // namespace openlr

View file

@ -0,0 +1,325 @@
#include "openlr/score_candidate_paths_getter.hpp"
#include "openlr/graph.hpp"
#include "openlr/openlr_model.hpp"
#include "openlr/score_candidate_points_getter.hpp"
#include "routing/road_graph.hpp"
#include "platform/location.hpp"
#include "geometry/angles.hpp"
#include "geometry/mercator.hpp"
#include "geometry/point_with_altitude.hpp"
#include "base/logging.hpp"
#include "base/stl_helpers.hpp"
#include <algorithm>
#include <functional>
#include <iterator>
#include <queue>
#include <set>
#include <tuple>
#include <utility>
using namespace routing;
using namespace std;
namespace openlr
{
namespace scpg
{
int constexpr kNumBuckets = 256;
double constexpr kAnglesInBucket = 360.0 / kNumBuckets;
double ToAngleInDeg(uint32_t angleInBuckets)
{
CHECK_LESS_OR_EQUAL(angleInBuckets, 255, ());
return math::Clamp(kAnglesInBucket * static_cast<double>(angleInBuckets), 0.0, 360.0);
}
uint32_t BearingInDeg(m2::PointD const & a, m2::PointD const & b)
{
auto const angle = location::AngleToBearing(math::RadToDeg(ang::AngleTo(a, b)));
CHECK(0.0 <= angle && angle <= 360.0, (angle));
return angle;
}
double DifferenceInDeg(double a1, double a2)
{
auto const diff = 180.0 - abs(abs(a1 - a2) - 180.0);
CHECK(0.0 <= diff && diff <= 180.0, (diff));
return diff;
}
void EdgeSortUniqueByStartAndEndPoints(Graph::EdgeListT & edges)
{
base::SortUnique(edges,
[](Edge const & e1, Edge const & e2)
{
if (e1.GetStartPoint() != e2.GetStartPoint())
return e1.GetStartPoint() < e2.GetStartPoint();
return e1.GetEndPoint() < e2.GetEndPoint();
}, [](Edge const & e1, Edge const & e2)
{ return e1.GetStartPoint() == e2.GetStartPoint() && e1.GetEndPoint() == e2.GetEndPoint(); });
}
} // namespace scpg
// ScoreCandidatePathsGetter::Link ----------------------------------------------------------------------
Graph::Edge ScoreCandidatePathsGetter::Link::GetStartEdge() const
{
auto * start = this;
while (start->m_parent)
start = start->m_parent.get();
return start->m_edge;
}
bool ScoreCandidatePathsGetter::Link::IsJunctionInPath(geometry::PointWithAltitude const & j) const
{
for (auto * l = this; l; l = l->m_parent.get())
{
if (l->m_edge.GetEndJunction().GetPoint() == j.GetPoint())
{
LOG(LDEBUG, ("A loop detected, skipping..."));
return true;
}
}
return false;
}
// ScoreCandidatePathsGetter ----------------------------------------------------------------------------
bool ScoreCandidatePathsGetter::GetLineCandidatesForPoints(vector<LocationReferencePoint> const & points,
LinearSegmentSource source,
vector<ScorePathVec> & lineCandidates)
{
CHECK_GREATER(points.size(), 1, ());
for (size_t i = 0; i < points.size(); ++i)
{
if (i != points.size() - 1 && points[i].m_distanceToNextPoint == 0)
{
LOG(LINFO, ("Distance to next point is zero. Skipping the whole segment"));
++m_stats.m_zeroDistToNextPointCount;
return false;
}
lineCandidates.emplace_back();
auto const isLastPoint = i == points.size() - 1;
double const distanceToNextPointM = (isLastPoint ? points[i - 1] : points[i]).m_distanceToNextPoint;
ScoreEdgeVec edgesCandidates;
m_pointsGetter.GetEdgeCandidates(mercator::FromLatLon(points[i].m_latLon), isLastPoint, edgesCandidates);
GetLineCandidates(points[i], source, isLastPoint, distanceToNextPointM, edgesCandidates, lineCandidates.back());
if (lineCandidates.back().empty())
{
LOG(LINFO, ("No candidate lines found for point", points[i].m_latLon, "Giving up"));
++m_stats.m_noCandidateFound;
return false;
}
}
CHECK_EQUAL(lineCandidates.size(), points.size(), ());
return true;
}
void ScoreCandidatePathsGetter::GetAllSuitablePaths(ScoreEdgeVec const & startLines, LinearSegmentSource source,
bool isLastPoint, double bearDistM,
FunctionalRoadClass functionalRoadClass, FormOfWay formOfWay,
double distanceToNextPointM, vector<shared_ptr<Link>> & allPaths)
{
CHECK_NOT_EQUAL(source, LinearSegmentSource::NotValid, ());
queue<shared_ptr<Link>> q;
for (auto const & e : startLines)
{
Score roadScore = 0; // Score based on functional road class and form of way.
if (source == LinearSegmentSource::FromLocationReferenceTag &&
!PassesRestrictionV3(e.m_edge, functionalRoadClass, formOfWay, m_infoGetter, roadScore))
{
continue;
}
q.push(make_shared<Link>(nullptr /* parent */, e.m_edge, 0 /* distanceM */, e.m_score, roadScore));
}
// Filling |allPaths| staring from |startLines| which have passed functional road class
// and form of way restrictions. All paths in |allPaths| are shorter then |bearDistM| plus
// one segment length.
while (!q.empty())
{
auto const u = q.front();
q.pop();
auto const & currentEdge = u->m_edge;
auto const currentEdgeLen = EdgeLength(currentEdge);
// The path from the start of the openlr segment to the finish to the openlr segment should be
// much shorter then the distance of any connection of openlr segment.
// This condition should be checked because otherwise in rare case |q| may be overfilled.
if (u->m_distanceM > distanceToNextPointM)
continue;
if (u->m_distanceM + currentEdgeLen >= bearDistM)
{
allPaths.emplace_back(std::move(u));
continue;
}
CHECK_LESS(u->m_distanceM + currentEdgeLen, bearDistM, ());
Graph::EdgeListT edges;
if (!isLastPoint)
m_graph.GetOutgoingEdges(currentEdge.GetEndJunction(), edges);
else
m_graph.GetIngoingEdges(currentEdge.GetStartJunction(), edges);
// It's possible that road edges are duplicated a lot of times. It's a error but
// a mapper may do that. To prevent a combinatorial explosion while matching
// duplicated edges should be removed.
scpg::EdgeSortUniqueByStartAndEndPoints(edges);
for (auto const & e : edges)
{
CHECK(!e.IsFake(), ());
if (EdgesAreAlmostEqual(e.GetReverseEdge(), currentEdge))
continue;
CHECK(currentEdge.HasRealPart(), ());
Score roadScore = 0;
if (source == LinearSegmentSource::FromLocationReferenceTag &&
!PassesRestrictionV3(e, functionalRoadClass, formOfWay, m_infoGetter, roadScore))
{
continue;
}
if (u->IsJunctionInPath(e.GetEndJunction()))
continue;
// Road score for a path is minimum value of score of segments based on functional road class
// of the segments and form of way of the segments.
q.emplace(
make_shared<Link>(u, e, u->m_distanceM + currentEdgeLen, u->m_pointScore, min(roadScore, u->m_minRoadScore)));
}
}
}
void ScoreCandidatePathsGetter::GetBestCandidatePaths(vector<shared_ptr<Link>> const & allPaths,
LinearSegmentSource source, bool isLastPoint,
uint32_t requiredBearing, double bearDistM,
m2::PointD const & startPoint, ScorePathVec & candidates)
{
CHECK_NOT_EQUAL(source, LinearSegmentSource::NotValid, ());
CHECK_LESS_OR_EQUAL(requiredBearing, 255, ());
multiset<CandidatePath, greater<>> candidatePaths;
BearingPointsSelector pointsSelector(static_cast<uint32_t>(bearDistM), isLastPoint);
for (auto const & link : allPaths)
{
auto const bearStartPoint = pointsSelector.GetStartPoint(link->GetStartEdge());
// Number of edges counting from the last one to check bearing on. According to OpenLR spec
// we have to check bearing on a point that is no longer than 25 meters traveling down the path.
// But sometimes we may skip the best place to stop and generate a candidate. So we check several
// edges before the last one to avoid such a situation. Number of iterations is taken
// by intuition.
// Example:
// o -------- o { Partners segment. }
// o ------- o --- o { Our candidate. }
// ^ 25m
// ^ This one may be better than
// ^ this one.
// So we want to check them all.
uint32_t traceBackIterationsLeft = 3;
for (auto part = link; part; part = part->m_parent)
{
if (traceBackIterationsLeft == 0)
break;
--traceBackIterationsLeft;
// Note. No information about bearing if source == LinearSegmentSource::FromCoordinatesTag.
Score bearingScore = 0;
if (source == LinearSegmentSource::FromLocationReferenceTag)
{
if (!GetBearingScore(pointsSelector, *part, bearStartPoint, requiredBearing, bearingScore))
continue;
}
candidatePaths.emplace(part, part->m_pointScore, part->m_minRoadScore, bearingScore);
}
}
size_t constexpr kMaxCandidates = 7;
vector<CandidatePath> paths;
copy_n(candidatePaths.begin(), min(static_cast<size_t>(kMaxCandidates), candidatePaths.size()), back_inserter(paths));
for (auto const & path : paths)
{
Graph::EdgeVector edges;
for (auto * p = path.m_path.get(); p; p = p->m_parent.get())
edges.push_back(p->m_edge);
if (!isLastPoint)
reverse(edges.begin(), edges.end());
candidates.emplace_back(path.GetScore(), std::move(edges));
}
}
void ScoreCandidatePathsGetter::GetLineCandidates(openlr::LocationReferencePoint const & p, LinearSegmentSource source,
bool isLastPoint, double distanceToNextPointM,
ScoreEdgeVec const & edgeCandidates, ScorePathVec & candidates)
{
double constexpr kDefaultBearDistM = 25.0;
double const bearDistM = min(kDefaultBearDistM, distanceToNextPointM);
ScoreEdgeVec const & startLines = edgeCandidates;
LOG(LDEBUG, ("Listing start lines:"));
for (auto const & e : startLines)
LOG(LDEBUG, (LogAs2GisPath(e.m_edge)));
auto const startPoint = mercator::FromLatLon(p.m_latLon);
vector<shared_ptr<Link>> allPaths;
GetAllSuitablePaths(startLines, source, isLastPoint, bearDistM, p.m_functionalRoadClass, p.m_formOfWay,
distanceToNextPointM, allPaths);
GetBestCandidatePaths(allPaths, source, isLastPoint, p.m_bearing, bearDistM, startPoint, candidates);
// Sorting by increasing order.
sort(candidates.begin(), candidates.end(),
[](ScorePath const & s1, ScorePath const & s2) { return s1.m_score > s2.m_score; });
LOG(LDEBUG, (candidates.size(), "Candidate paths found for point:", p.m_latLon));
}
bool ScoreCandidatePathsGetter::GetBearingScore(BearingPointsSelector const & pointsSelector,
ScoreCandidatePathsGetter::Link const & part,
m2::PointD const & bearStartPoint, uint32_t requiredBearing,
Score & score)
{
auto const bearEndPoint = pointsSelector.GetEndPoint(part.m_edge, part.m_distanceM);
auto const bearingDeg = scpg::BearingInDeg(bearStartPoint, bearEndPoint);
double const requiredBearingDeg = scpg::ToAngleInDeg(requiredBearing);
double const angleDeviationDeg = scpg::DifferenceInDeg(bearingDeg, requiredBearingDeg);
// If the bearing according to osm segments (|bearingDeg|) is significantly different
// from the bearing set in openlr (|requiredBearingDeg|) the candidate should be skipped.
double constexpr kMinAngleDeviationDeg = 50.0;
if (angleDeviationDeg > kMinAngleDeviationDeg)
return false;
double constexpr kMaxScoreForBearing = 60.0;
double constexpr kAngleDeviationFactor = 1.0 / 4.3;
score = static_cast<Score>(kMaxScoreForBearing / (1.0 + angleDeviationDeg * kAngleDeviationFactor));
return true;
}
} // namespace openlr

View file

@ -0,0 +1,119 @@
#pragma once
#include "openlr/graph.hpp"
#include "openlr/helpers.hpp"
#include "openlr/openlr_model.hpp"
#include "openlr/road_info_getter.hpp"
#include "openlr/score_types.hpp"
#include "openlr/stats.hpp"
#include "geometry/point2d.hpp"
#include "base/assert.hpp"
#include <cstdint>
#include <limits>
#include <memory>
#include <vector>
namespace openlr
{
class ScoreCandidatePointsGetter;
class ScoreCandidatePathsGetter
{
public:
ScoreCandidatePathsGetter(ScoreCandidatePointsGetter & pointsGetter, Graph & graph, RoadInfoGetter & infoGetter,
v2::Stats & stat)
: m_pointsGetter(pointsGetter)
, m_graph(graph)
, m_infoGetter(infoGetter)
, m_stats(stat)
{}
bool GetLineCandidatesForPoints(std::vector<LocationReferencePoint> const & points, LinearSegmentSource source,
std::vector<ScorePathVec> & lineCandidates);
private:
struct Link
{
Link(std::shared_ptr<Link> const & parent, Graph::Edge const & edge, double distanceM, Score pointScore,
Score rfcScore)
: m_parent(parent)
, m_edge(edge)
, m_distanceM(distanceM)
, m_pointScore(pointScore)
, m_minRoadScore(rfcScore)
{
CHECK(!edge.IsFake(), ("Edge should not be fake:", edge));
}
bool IsJunctionInPath(geometry::PointWithAltitude const & j) const;
Graph::Edge GetStartEdge() const;
std::shared_ptr<Link> const m_parent;
Graph::Edge const m_edge;
double const m_distanceM;
Score const m_pointScore;
// Minimum score of segments of the path going along |m_parent| based on functional road class
// and form of way.
Score const m_minRoadScore;
};
struct CandidatePath
{
CandidatePath() = default;
CandidatePath(std::shared_ptr<Link> const path, Score pointScore, Score rfcScore, Score bearingScore)
: m_path(path)
, m_pointScore(pointScore)
, m_roadScore(rfcScore)
, m_bearingScore(bearingScore)
{}
bool operator>(CandidatePath const & o) const { return GetScore() > o.GetScore(); }
Score GetScore() const { return m_pointScore + m_roadScore + m_bearingScore; }
std::shared_ptr<Link> m_path = nullptr;
Score m_pointScore = 0;
Score m_roadScore = 0;
Score m_bearingScore = 0;
};
// Note: In all methods below if |isLastPoint| is true than algorithm should
// calculate all parameters (such as bearing, distance to next point, etc.)
// relative to the last point.
// o ----> o ----> o <---- o.
// 1 2 3 4
// ^ isLastPoint = true.
// To calculate bearing for points 1 to 3 one have to go beardist from
// previous point to the next one (eg. from 1 to 2 and from 2 to 3).
// For point 4 one have to go from 4 to 3 reversing directions. And
// distance-to-next point is taken from point 3. You can learn more in
// TomTom OpenLR spec.
/// \brief Fills |allPaths| with paths near start or finish point starting from |startLines|.
/// To extract a path from |allPaths| a item from |allPaths| should be taken,
/// then should be taken the member |m_parent| of the item and so on till the beginning.
void GetAllSuitablePaths(ScoreEdgeVec const & startLines, LinearSegmentSource source, bool isLastPoint,
double bearDistM, FunctionalRoadClass functionalRoadClass, FormOfWay formOfWay,
double distanceToNextPointM, std::vector<std::shared_ptr<Link>> & allPaths);
void GetBestCandidatePaths(std::vector<std::shared_ptr<Link>> const & allPaths, LinearSegmentSource source,
bool isLastPoint, uint32_t requiredBearing, double bearDistM,
m2::PointD const & startPoint, ScorePathVec & candidates);
void GetLineCandidates(openlr::LocationReferencePoint const & p, LinearSegmentSource source, bool isLastPoint,
double distanceToNextPointM, ScoreEdgeVec const & edgeCandidates, ScorePathVec & candidates);
bool GetBearingScore(BearingPointsSelector const & pointsSelector, ScoreCandidatePathsGetter::Link const & part,
m2::PointD const & bearStartPoint, uint32_t requiredBearing, Score & score);
ScoreCandidatePointsGetter & m_pointsGetter;
Graph & m_graph;
RoadInfoGetter & m_infoGetter;
v2::Stats & m_stats;
};
} // namespace openlr

View file

@ -0,0 +1,137 @@
#include "openlr/score_candidate_points_getter.hpp"
#include "openlr/helpers.hpp"
#include "routing/road_graph.hpp"
#include "routing/routing_helpers.hpp"
#include "storage/country_info_getter.hpp"
#include "indexer/feature.hpp"
#include "indexer/feature_data.hpp"
#include "indexer/feature_decl.hpp"
#include "indexer/scales.hpp"
#include "geometry/mercator.hpp"
#include "geometry/point_with_altitude.hpp"
#include "base/assert.hpp"
#include "base/stl_helpers.hpp"
#include <algorithm>
#include <set>
#include <utility>
using namespace routing;
namespace
{
// Ends of segments and intermediate points of segments are considered only within this radius.
double const kRadius = 30.0;
} // namespace
namespace openlr
{
void ScoreCandidatePointsGetter::GetJunctionPointCandidates(m2::PointD const & p, bool isLastPoint,
ScoreEdgeVec & edgeCandidates)
{
ScorePointVec pointCandidates;
auto const selectCandidates = [&p, &pointCandidates, this](FeatureType & ft)
{
ft.ParseGeometry(FeatureType::BEST_GEOMETRY);
if (ft.GetGeomType() != feature::GeomType::Line || !routing::IsRoad(feature::TypesHolder(ft)))
return;
ft.ForEachPoint([&p, &pointCandidates, this](m2::PointD const & candidate)
{
if (mercator::DistanceOnEarth(p, candidate) < kRadius)
pointCandidates.emplace_back(GetScoreByDistance(p, candidate), candidate);
}, scales::GetUpperScale());
};
m_dataSource.ForEachInRect(selectCandidates, mercator::RectByCenterXYAndSizeInMeters(p, kRadius),
scales::GetUpperScale());
base::SortUnique(pointCandidates);
std::reverse(pointCandidates.begin(), pointCandidates.end());
pointCandidates.resize(std::min(m_maxJunctionCandidates, pointCandidates.size()));
for (auto const & pc : pointCandidates)
{
Graph::EdgeListT edges;
if (!isLastPoint)
m_graph.GetOutgoingEdges(geometry::PointWithAltitude(pc.m_point, 0 /* altitude */), edges);
else
m_graph.GetIngoingEdges(geometry::PointWithAltitude(pc.m_point, 0 /* altitude */), edges);
for (auto const & e : edges)
edgeCandidates.emplace_back(pc.m_score, e);
}
}
void ScoreCandidatePointsGetter::EnrichWithProjectionPoints(m2::PointD const & p, ScoreEdgeVec & edgeCandidates)
{
m_graph.ResetFakes();
std::vector<std::pair<Graph::Edge, geometry::PointWithAltitude>> vicinities;
m_graph.FindClosestEdges(p, static_cast<uint32_t>(m_maxProjectionCandidates), vicinities);
for (auto const & v : vicinities)
{
auto const & edge = v.first;
auto const & proj = v.second;
CHECK(edge.HasRealPart(), ());
CHECK(!edge.IsFake(), ());
if (mercator::DistanceOnEarth(p, proj.GetPoint()) >= kRadius)
continue;
edgeCandidates.emplace_back(GetScoreByDistance(p, proj.GetPoint()), edge);
}
}
bool ScoreCandidatePointsGetter::IsJunction(m2::PointD const & p)
{
Graph::EdgeListT outgoing;
m_graph.GetRegularOutgoingEdges(geometry::PointWithAltitude(p, 0 /* altitude */), outgoing);
Graph::EdgeListT ingoing;
m_graph.GetRegularIngoingEdges(geometry::PointWithAltitude(p, 0 /* altitude */), ingoing);
// Note. At mwm borders the size of |ids| may be bigger than two in case of straight
// road because of road feature duplication at borders.
std::set<std::pair<uint32_t, uint32_t>> ids;
for (auto const & e : outgoing)
ids.insert(std::make_pair(e.GetFeatureId().m_index, e.GetSegId()));
for (auto const & e : ingoing)
ids.insert(std::make_pair(e.GetFeatureId().m_index, e.GetSegId()));
// Size of |ids| is number of different pairs of (feature id, segment id) starting from
// |p| plus going to |p|. The size 0, 1 or 2 is considered |p| is not a junction of roads.
// If the size is 3 or more it means |p| is an intersection of 3 or more roads.
return ids.size() >= 3;
}
Score ScoreCandidatePointsGetter::GetScoreByDistance(m2::PointD const & point, m2::PointD const & candidate)
{
// Maximum possible score for the distance between an openlr segment ends and an osm segments.
Score constexpr kMaxScoreForDist = 70;
// If the distance between an openlr segments end and an osm segments end is less or equal
// |kMaxScoreDistM| the point gets |kMaxScoreForDist| score.
double constexpr kMaxScoreDistM = 5.0;
// According to the standard openlr edge should be started from a junction. Despite the fact
// that openlr and osm are based on different graphs, the score of junction should be increased.
double const junctionFactor = IsJunction(candidate) ? 1.1 : 1.0;
double const distM = mercator::DistanceOnEarth(point, candidate);
double const score = distM <= kMaxScoreDistM
? kMaxScoreForDist * junctionFactor
: static_cast<double>(kMaxScoreForDist) * junctionFactor / (1.0 + distM - kMaxScoreDistM);
CHECK_GREATER_OR_EQUAL(score, 0.0, ());
CHECK_LESS_OR_EQUAL(score, static_cast<double>(kMaxScoreForDist) * junctionFactor, ());
return static_cast<Score>(score);
}
} // namespace openlr

View file

@ -0,0 +1,48 @@
#pragma once
#include "openlr/graph.hpp"
#include "openlr/score_types.hpp"
#include "openlr/stats.hpp"
#include "indexer/data_source.hpp"
#include "geometry/point2d.hpp"
#include <cstdint>
#include <functional>
#include <vector>
namespace openlr
{
class ScoreCandidatePointsGetter
{
public:
ScoreCandidatePointsGetter(size_t maxJunctionCandidates, size_t maxProjectionCandidates,
DataSource const & dataSource, Graph & graph)
: m_maxJunctionCandidates(maxJunctionCandidates)
, m_maxProjectionCandidates(maxProjectionCandidates)
, m_dataSource(dataSource)
, m_graph(graph)
{}
void GetEdgeCandidates(m2::PointD const & p, bool isLastPoint, ScoreEdgeVec & edges)
{
GetJunctionPointCandidates(p, isLastPoint, edges);
EnrichWithProjectionPoints(p, edges);
}
private:
void GetJunctionPointCandidates(m2::PointD const & p, bool isLastPoint, ScoreEdgeVec & edgeCandidates);
void EnrichWithProjectionPoints(m2::PointD const & p, ScoreEdgeVec & edgeCandidates);
/// \returns true if |p| is a junction and false otherwise.
bool IsJunction(m2::PointD const & p);
Score GetScoreByDistance(m2::PointD const & point, m2::PointD const & candidate);
size_t const m_maxJunctionCandidates;
size_t const m_maxProjectionCandidates;
DataSource const & m_dataSource;
Graph & m_graph;
};
} // namespace openlr

View file

@ -0,0 +1,349 @@
#include "openlr/score_paths_connector.hpp"
#include "openlr/helpers.hpp"
#include "indexer/feature_data.hpp"
#include "indexer/ftypes_matcher.hpp"
#include "base/assert.hpp"
#include "base/checked_cast.hpp"
#include "base/stl_helpers.hpp"
#include "base/stl_iterator.hpp"
#include <algorithm>
#include <cmath>
#include <map>
#include <queue>
#include <string>
#include <tuple>
using namespace std;
namespace openlr
{
namespace
{
class EdgeContainer
{
public:
explicit EdgeContainer(Graph const & graph) : m_graph(graph) {}
void ProcessEdge(Graph::Edge const & edge)
{
CHECK(!edge.IsFake(), ());
feature::TypesHolder types;
m_graph.GetFeatureTypes(edge.GetFeatureId(), types);
ftypes::HighwayClass const hwClass = ftypes::GetHighwayClass(types);
if (m_minHwClass == ftypes::HighwayClass::Undefined)
{
m_minHwClass = hwClass;
m_maxHwClass = m_minHwClass;
m_oneWay.m_field = ftypes::IsOneWayChecker::Instance()(types);
m_roundabout.m_field = ftypes::IsRoundAboutChecker::Instance()(types);
m_link.m_field = ftypes::IsLinkChecker::Instance()(types);
}
else
{
CHECK(hwClass != ftypes::HighwayClass::Undefined, (edge));
m_minHwClass = static_cast<ftypes::HighwayClass>(min(base::Underlying(m_minHwClass), base::Underlying(hwClass)));
m_maxHwClass = static_cast<ftypes::HighwayClass>(max(base::Underlying(m_maxHwClass), base::Underlying(hwClass)));
if (m_oneWay.m_allEqual && m_oneWay.m_field != ftypes::IsOneWayChecker::Instance()(types))
m_oneWay.m_allEqual = false;
if (m_roundabout.m_allEqual && m_roundabout.m_field != ftypes::IsRoundAboutChecker::Instance()(types))
m_roundabout.m_allEqual = false;
if (m_link.m_allEqual && m_link.m_field != ftypes::IsLinkChecker::Instance()(types))
m_link.m_allEqual = false;
}
}
uint8_t GetHighwayClassDiff() const
{
CHECK_NOT_EQUAL(m_minHwClass, ftypes::HighwayClass::Undefined, ());
CHECK_GREATER_OR_EQUAL(m_maxHwClass, m_minHwClass, ());
uint8_t const hwClassDiff = static_cast<uint8_t>(m_maxHwClass) - static_cast<uint8_t>(m_minHwClass);
return hwClassDiff;
}
bool AreAllOneWaysEqual() const { return m_oneWay.m_allEqual; }
bool AreAllRoundaboutEqual() const { return m_roundabout.m_allEqual; }
bool AreAllLinksEqual() const { return m_link.m_allEqual; }
private:
struct Field
{
bool m_field = false;
bool m_allEqual = true;
};
Graph const & m_graph;
ftypes::HighwayClass m_minHwClass = ftypes::HighwayClass::Undefined;
ftypes::HighwayClass m_maxHwClass = ftypes::HighwayClass::Undefined;
Field m_oneWay;
Field m_roundabout;
Field m_link;
};
/// \returns true if |path| may be used as a candidate. In that case |lenScore| is filled
/// with score of this candidate based on length. The closer length of the |path| to
/// |distanceToNextPoint| the more score.
bool ValidatePathByLength(Graph::EdgeVector const & path, double distanceToNextPoint, LinearSegmentSource source,
Score & lenScore)
{
CHECK(!path.empty(), ());
CHECK_NOT_EQUAL(source, LinearSegmentSource::NotValid, ());
Score const kMaxScoreForRouteLen = 110;
double pathLen = 0.0;
for (auto const & e : path)
pathLen += EdgeLength(e);
// 0 <= |pathDiffRatio| <= 1. The more pathDiffRatio the closer |distanceToNextPoint| and |pathLen|.
double const pathDiffRatio = 1.0 - abs(distanceToNextPoint - pathLen) / max(distanceToNextPoint, pathLen);
bool const shortPath = path.size() <= 2;
double const kMinValidPathDiffRation = source == LinearSegmentSource::FromLocationReferenceTag ? 0.6 : 0.25;
if (pathDiffRatio <= kMinValidPathDiffRation && !shortPath)
return false;
lenScore = static_cast<Score>(kMaxScoreForRouteLen * (pathDiffRatio - kMinValidPathDiffRation) /
(1.0 - kMinValidPathDiffRation));
return true;
}
bool AreEdgesEqualWithoutAltitude(Graph::Edge const & e1, Graph::Edge const & e2)
{
return make_tuple(e1.GetType(), e1.GetFeatureId(), e1.IsForward(), e1.GetSegId(), e1.GetStartPoint(),
e1.GetEndPoint()) == make_tuple(e2.GetType(), e2.GetFeatureId(), e2.IsForward(), e2.GetSegId(),
e2.GetStartPoint(), e2.GetEndPoint());
}
} // namespace
ScorePathsConnector::ScorePathsConnector(Graph & graph, RoadInfoGetter & infoGetter, v2::Stats & stat)
: m_graph(graph)
, m_infoGetter(infoGetter)
, m_stat(stat)
{}
bool ScorePathsConnector::FindBestPath(vector<LocationReferencePoint> const & points,
vector<vector<ScorePath>> const & lineCandidates, LinearSegmentSource source,
vector<Graph::EdgeVector> & resultPath)
{
CHECK_NOT_EQUAL(source, LinearSegmentSource::NotValid, ());
CHECK_GREATER_OR_EQUAL(points.size(), 2, ());
resultPath.resize(points.size() - 1);
for (size_t i = 1; i < points.size(); ++i)
{
// @TODO It's possible that size of |points| is more then two. In that case two or more edges
// should be approximated with routes. If so only |toCandidates| which may be reached from
// |fromCandidates| should be used for the next edge.
auto const & point = points[i - 1];
auto const distanceToNextPoint = static_cast<double>(point.m_distanceToNextPoint);
auto const & fromCandidates = lineCandidates[i - 1];
auto const & toCandidates = lineCandidates[i];
auto & resultPathPart = resultPath[i - 1];
vector<ScorePath> result;
for (size_t fromInd = 0; fromInd < fromCandidates.size(); ++fromInd)
{
for (size_t toInd = 0; toInd < toCandidates.size(); ++toInd)
{
Graph::EdgeVector path;
if (!ConnectAdjacentCandidateLines(fromCandidates[fromInd].m_path, toCandidates[toInd].m_path, source,
point.m_lfrcnp, distanceToNextPoint, path))
{
continue;
}
Score pathLenScore = 0;
if (!ValidatePathByLength(path, distanceToNextPoint, source, pathLenScore))
continue;
auto const score =
pathLenScore + GetScoreForUniformity(path) + fromCandidates[fromInd].m_score + toCandidates[toInd].m_score;
result.emplace_back(score, std::move(path));
}
}
for (auto const & p : result)
CHECK(!p.m_path.empty(), ());
if (result.empty())
{
LOG(LINFO, ("No shortest path found"));
++m_stat.m_noShortestPathFound;
resultPathPart.clear();
return false;
}
auto const it = max_element(result.cbegin(), result.cend(),
[](ScorePath const & o1, ScorePath const & o2) { return o1.m_score < o2.m_score; });
// Note. In case of source == LinearSegmentSource::FromCoordinatesTag there is less
// information about a openlr segment so less score is collected.
Score const kMinValidScore = source == LinearSegmentSource::FromLocationReferenceTag ? 240 : 165;
if (it->m_score < kMinValidScore)
{
LOG(LINFO, ("The shortest path found but it is not good. The best score:", it->m_score));
++m_stat.m_notEnoughScore;
return false;
}
resultPathPart = it->m_path;
LOG(LDEBUG, ("Best score:", it->m_score, "resultPathPart.size():", resultPathPart.size()));
}
CHECK_EQUAL(resultPath.size(), points.size() - 1, ());
return true;
}
bool ScorePathsConnector::FindShortestPath(Graph::Edge const & from, Graph::Edge const & to, LinearSegmentSource source,
FunctionalRoadClass lowestFrcToNextPoint, uint32_t maxPathLength,
Graph::EdgeVector & path)
{
double constexpr kLengthToleranceFactor = 1.1;
uint32_t constexpr kMinLengthTolerance = 20;
uint32_t const lengthToleranceM =
max(static_cast<uint32_t>(kLengthToleranceFactor * maxPathLength), kMinLengthTolerance);
struct State
{
State(Graph::Edge const & e, uint32_t const s) : m_edge(e), m_score(s) {}
bool operator>(State const & o) const { return tie(m_score, m_edge) > tie(o.m_score, o.m_edge); }
Graph::Edge m_edge;
uint32_t m_score;
};
CHECK(from.HasRealPart() && to.HasRealPart(), ());
priority_queue<State, vector<State>, greater<>> q;
map<Graph::Edge, double> scores;
map<Graph::Edge, Graph::Edge> links;
q.emplace(from, 0);
scores[from] = 0;
while (!q.empty())
{
auto const state = q.top();
q.pop();
auto const & stateEdge = state.m_edge;
// TODO(mgsergio): Unify names: use either score or distance.
auto const stateScore = state.m_score;
if (stateScore > maxPathLength + lengthToleranceM)
continue;
if (stateScore > scores[stateEdge])
continue;
if (AreEdgesEqualWithoutAltitude(stateEdge, to))
{
for (auto edge = stateEdge; edge != from; edge = links[edge])
path.emplace_back(edge);
path.emplace_back(from);
reverse(begin(path), end(path));
return true;
}
Graph::EdgeListT edges;
m_graph.GetOutgoingEdges(stateEdge.GetEndJunction(), edges);
for (auto const & edge : edges)
{
if (source == LinearSegmentSource::FromLocationReferenceTag &&
!ConformLfrcnpV3(edge, lowestFrcToNextPoint, m_infoGetter))
{
continue;
}
CHECK(!stateEdge.IsFake(), ());
CHECK(!edge.IsFake(), ());
auto const it = scores.find(edge);
auto const eScore = stateScore + EdgeLength(edge);
if (it == end(scores) || it->second > eScore)
{
scores[edge] = eScore;
links[edge] = stateEdge;
q.emplace(edge, eScore);
}
}
}
return false;
}
bool ScorePathsConnector::ConnectAdjacentCandidateLines(Graph::EdgeVector const & from, Graph::EdgeVector const & to,
LinearSegmentSource source,
FunctionalRoadClass lowestFrcToNextPoint,
double distanceToNextPoint, Graph::EdgeVector & resultPath)
{
CHECK(!to.empty(), ());
if (auto const skip = PathOverlappingLen(from, to))
{
if (skip == -1)
return false;
resultPath.insert(resultPath.end(), from.cbegin(), from.cend());
resultPath.insert(resultPath.end(), to.cbegin() + skip, to.cend());
return true;
}
CHECK_NOT_EQUAL(from, to, ());
Graph::EdgeVector shortestPath;
auto const found = FindShortestPath(from.back(), to.front(), source, lowestFrcToNextPoint,
static_cast<uint32_t>(ceil(distanceToNextPoint)), shortestPath);
if (!found)
return false;
CHECK_EQUAL(from.back(), shortestPath.front(), ());
resultPath.insert(resultPath.end(), from.cbegin(), prev(from.cend()));
resultPath.insert(resultPath.end(), shortestPath.cbegin(), shortestPath.cend());
CHECK(AreEdgesEqualWithoutAltitude(to.front(), shortestPath.back()), (to.front(), shortestPath.back()));
resultPath.insert(resultPath.end(), next(to.begin()), to.end());
return !resultPath.empty();
}
Score ScorePathsConnector::GetScoreForUniformity(Graph::EdgeVector const & path)
{
EdgeContainer edgeContainer(m_graph);
for (auto const & edge : path)
edgeContainer.ProcessEdge(edge);
auto const hwClassDiff = edgeContainer.GetHighwayClassDiff();
Score constexpr kScoreForTheSameHwClass = 40;
Score constexpr kScoreForNeighboringHwClasses = 15;
Score const hwClassScore = hwClassDiff == 0 ? kScoreForTheSameHwClass
: hwClassDiff == 1 ? kScoreForNeighboringHwClasses
: 0;
Score constexpr kScoreForOneWayOnly = 17;
Score constexpr kScoreForRoundaboutOnly = 18;
Score constexpr kScoreForLinkOnly = 10;
Score const allEqualScore = (edgeContainer.AreAllOneWaysEqual() ? kScoreForOneWayOnly : 0) +
(edgeContainer.AreAllRoundaboutEqual() ? kScoreForRoundaboutOnly : 0) +
(edgeContainer.AreAllLinksEqual() ? kScoreForLinkOnly : 0);
return hwClassScore + allEqualScore;
}
} // namespace openlr

View file

@ -0,0 +1,41 @@
#pragma once
#include "openlr/graph.hpp"
#include "openlr/openlr_model.hpp"
#include "openlr/road_info_getter.hpp"
#include "openlr/score_types.hpp"
#include "openlr/stats.hpp"
#include <cstddef>
#include <cstdint>
#include <vector>
namespace openlr
{
class ScorePathsConnector
{
public:
ScorePathsConnector(Graph & graph, RoadInfoGetter & infoGetter, v2::Stats & stat);
/// \brief Connects |lineCandidates| and fills |resultPath| with the path with maximum score
/// if there's a good enough.
/// \returns true if the best path is found and false otherwise.
bool FindBestPath(std::vector<LocationReferencePoint> const & points,
std::vector<std::vector<ScorePath>> const & lineCandidates, LinearSegmentSource source,
std::vector<Graph::EdgeVector> & resultPath);
private:
bool FindShortestPath(Graph::Edge const & from, Graph::Edge const & to, LinearSegmentSource source,
FunctionalRoadClass lowestFrcToNextPoint, uint32_t maxPathLength, Graph::EdgeVector & path);
bool ConnectAdjacentCandidateLines(Graph::EdgeVector const & from, Graph::EdgeVector const & to,
LinearSegmentSource source, FunctionalRoadClass lowestFrcToNextPoint,
double distanceToNextPoint, Graph::EdgeVector & resultPath);
Score GetScoreForUniformity(Graph::EdgeVector const & path);
Graph & m_graph;
RoadInfoGetter & m_infoGetter;
v2::Stats & m_stat;
};
} // namespace openlr

View file

@ -0,0 +1,52 @@
#pragma once
#include "routing/road_graph.hpp"
#include "geometry/point2d.hpp"
#include <cstdint>
#include <tuple>
#include <vector>
namespace openlr
{
using Edge = routing::Edge;
using EdgeVector = routing::RoadGraphBase::EdgeVector;
using Score = uint32_t;
struct ScorePoint
{
ScorePoint() = default;
ScorePoint(Score score, m2::PointD const & point) : m_score(score), m_point(point) {}
bool operator<(ScorePoint const & o) const { return std::tie(m_score, m_point) < std::tie(o.m_score, o.m_point); }
bool operator==(ScorePoint const & o) const { return m_score == o.m_score && m_point == o.m_point; }
Score m_score = 0;
m2::PointD m_point;
};
using ScorePointVec = std::vector<ScorePoint>;
struct ScoreEdge
{
ScoreEdge(Score score, Edge const & edge) : m_score(score), m_edge(edge) {}
Score m_score;
Edge m_edge;
};
using ScoreEdgeVec = std::vector<ScoreEdge>;
struct ScorePath
{
ScorePath(Score score, EdgeVector && path) : m_score(score), m_path(std::move(path)) {}
Score m_score;
EdgeVector m_path;
};
using ScorePathVec = std::vector<ScorePath>;
} // namespace openlr

48
tools/openlr/stats.hpp Normal file
View file

@ -0,0 +1,48 @@
#pragma once
#include "openlr/cache_line_size.hpp"
#include "base/logging.hpp"
#include <chrono>
#include <cstdint>
namespace openlr
{
namespace v2
{
struct alignas(kCacheLineSize) Stats
{
void Add(Stats const & s)
{
m_routesHandled += s.m_routesHandled;
m_routesFailed += s.m_routesFailed;
m_noCandidateFound += s.m_noCandidateFound;
m_noShortestPathFound += s.m_noShortestPathFound;
m_notEnoughScore += s.m_notEnoughScore;
m_wrongOffsets += s.m_wrongOffsets;
m_zeroDistToNextPointCount += s.m_zeroDistToNextPointCount;
}
void Report() const
{
LOG(LINFO, ("Total routes handled:", m_routesHandled));
LOG(LINFO, ("Failed:", m_routesFailed));
LOG(LINFO, ("No candidate lines:", m_noCandidateFound));
LOG(LINFO, ("Wrong distance to next point:", m_zeroDistToNextPointCount));
LOG(LINFO, ("Not enough score for shortest path:", m_notEnoughScore));
LOG(LINFO, ("Wrong offsets:", m_wrongOffsets));
LOG(LINFO, ("No shortest path:", m_noShortestPathFound));
}
uint32_t m_routesHandled = 0;
uint32_t m_routesFailed = 0;
uint32_t m_noCandidateFound = 0;
uint32_t m_noShortestPathFound = 0;
uint32_t m_notEnoughScore = 0;
uint32_t m_wrongOffsets = 0;
// Number of zeroed distance-to-next point values in the input.
uint32_t m_zeroDistToNextPointCount = 0;
};
} // namespace v2
} // namespace openlr

View file

@ -0,0 +1,26 @@
#pragma once
#include "openlr/openlr_model.hpp"
#include "geometry/mercator.hpp"
#include "geometry/point2d.hpp"
#include <cstdint>
namespace openlr
{
struct WayPoint final
{
explicit WayPoint(openlr::LocationReferencePoint const & lrp)
: m_point(mercator::FromLatLon(lrp.m_latLon))
, m_distanceToNextPointM(lrp.m_distanceToNextPoint)
, m_bearing(lrp.m_bearing)
, m_lfrcnp(lrp.m_functionalRoadClass)
{}
m2::PointD m_point;
double m_distanceToNextPointM = 0.0;
uint8_t m_bearing = 0;
openlr::FunctionalRoadClass m_lfrcnp = openlr::FunctionalRoadClass::NotAValue;
};
} // namespace openlr

View file

@ -0,0 +1,17 @@
project(poly_borders)
set(
SRC
borders_data.cpp
borders_data.hpp
help_structures.cpp
help_structures.hpp
)
omim_add_library(${PROJECT_NAME} ${SRC})
target_link_libraries(${PROJECT_NAME} generator)
omim_add_test_subdirectory(poly_borders_tests)
omim_add_tool_subdirectory(poly_borders_tool)

View file

@ -0,0 +1,447 @@
#include "poly_borders/borders_data.hpp"
#include "poly_borders/help_structures.hpp"
#include "generator/borders.hpp"
#include "generator/routing_city_boundaries_processor.hpp"
#include "platform/platform.hpp"
#include "geometry/point2d.hpp"
#include "geometry/rect2d.hpp"
#include "geometry/region2d.hpp"
#include "base/assert.hpp"
#include "base/file_name_utils.hpp"
#include "base/logging.hpp"
#include "base/string_utils.hpp"
#include "base/thread_pool_computational.hpp"
#include <algorithm>
#include <iostream>
#include <tuple>
namespace
{
using namespace poly_borders;
void PrintWithSpaces(std::string const & str, size_t maxN)
{
std::cout << str;
if (maxN <= str.size())
return;
maxN -= str.size();
for (size_t i = 0; i < maxN; ++i)
std::cout << " ";
}
std::string RemoveIndexFromMwmName(std::string const & mwmName)
{
auto const pos = mwmName.find(BordersData::kBorderExtension);
CHECK_NOT_EQUAL(pos, std::string::npos, ());
auto const end = mwmName.begin() + pos + BordersData::kBorderExtension.size();
std::string result(mwmName.cbegin(), end);
return result;
}
bool IsReversedIntervals(size_t fromDst, size_t toDst, size_t fromSrc, size_t toSrc)
{
return (fromDst > toDst) != (fromSrc > toSrc);
}
std::vector<m2::PointD> AppendPointsWithAnyDirection(std::vector<MarkedPoint> const & copyFrom, size_t from, size_t to)
{
std::vector<m2::PointD> result;
if (from > to)
std::swap(from, to);
for (size_t k = from; k <= to; ++k)
result.emplace_back(copyFrom[k].m_point);
return result;
}
double AbsAreaDiff(std::vector<m2::PointD> const & firstPolygon, std::vector<m2::PointD> const & secondPolygon)
{
auto const firstArea = generator::AreaOnEarth(firstPolygon);
auto const secondArea = generator::AreaOnEarth(secondPolygon);
return std::abs(secondArea - firstArea);
}
bool NeedReplace(std::vector<m2::PointD> const & curSubpolygon, std::vector<m2::PointD> const & anotherSubpolygon)
{
auto const areaDiff = AbsAreaDiff(curSubpolygon, anotherSubpolygon);
double constexpr kMaxAreaDiffMetersSquared = 20000.0;
if (areaDiff > kMaxAreaDiffMetersSquared)
return false;
if (AlmostEqualAbs(areaDiff, 0.0, BordersData::kEqualityEpsilon))
return false;
// We know that |curSize| is always greater than 1, because we construct it such way, but we know
// nothing about |anotherSize|, and we do not want to replace current subpolygon of several points
// with a subpolygon consisting of one point or consisting of too many points.
CHECK_GREATER(curSubpolygon.size(), 1, ());
return 1 < anotherSubpolygon.size() && anotherSubpolygon.size() < 10;
}
bool ShouldLog(size_t i, size_t n)
{
return (i % 100 == 0) || (i + 1 == n);
}
void Append(size_t from, size_t to, bool reversed, std::vector<MarkedPoint> const & copyFrom,
std::vector<MarkedPoint> & copyTo)
{
if (!reversed)
{
for (size_t k = from; k <= to; ++k)
copyTo.emplace_back(copyFrom[k].m_point);
}
else
{
size_t k = to;
while (k >= from)
{
copyTo.emplace_back(copyFrom[k].m_point);
if (k == 0)
break;
--k;
}
}
}
m2::RectD BoundingBox(std::vector<MarkedPoint> const & points)
{
m2::RectD rect;
for (auto const & point : points)
rect.Add(point.m_point);
return rect;
}
void SwapIfNeeded(size_t & a, size_t & b)
{
if (a > b)
std::swap(a, b);
}
} // namespace
namespace poly_borders
{
// BordersData -------------------------------------------------------------------------------------
void BordersData::Init(std::string const & bordersDir)
{
LOG(LINFO, ("Borders path:", bordersDir));
// key - coordinates
// value - {border idx, point idx}
std::unordered_map<int64_t, std::vector<std::pair<size_t, size_t>>> index;
std::vector<std::string> files;
Platform::GetFilesByExt(bordersDir, kBorderExtension, files);
LOG(LINFO, ("Start reading data from .poly files."));
size_t prevIndex = 0;
for (auto const & file : files)
{
auto const fullPath = base::JoinPath(bordersDir, file);
size_t polygonId = 1;
borders::PolygonsList borders;
borders::LoadBorders(fullPath, borders);
for (auto & region : borders)
{
auto & points = region.MutableData();
m_duplicatedPointsCount += RemoveDuplicatingPointImpl(points);
CHECK_GREATER(points.size(), 1, (fullPath));
// Some mwms have several polygons. For example, for Japan_Kanto_Tokyo that has 2 polygons we
// will write 2 files:
// Japan_Kanto_Tokyo.poly1
// Japan_Kanto_Tokyo.poly2
auto const fileCopy = file + std::to_string(polygonId);
m_indexToPolyFileName[prevIndex] = fileCopy;
m_polyFileNameToIndex[fileCopy] = prevIndex++;
size_t const borderIdx = m_bordersPolygons.size();
for (size_t i = 0; i < points.size(); ++i)
index[PointToInt64Obsolete(points[i], kPointCoordBits)].emplace_back(borderIdx, i);
++polygonId;
m_bordersPolygons.emplace_back(region.GetRect(), points);
}
}
for (auto const & [_, v] : index)
{
for (size_t i = 0; i < v.size() - 1; ++i)
for (size_t j = i + 1; j < v.size(); ++j)
{
m_bordersPolygons[v[i].first].m_points[v[i].second].AddLink(v[j].first, v[j].second);
m_bordersPolygons[v[j].first].m_points[v[j].second].AddLink(v[i].first, v[i].second);
}
}
LOG(LINFO, ("Removed:", m_duplicatedPointsCount, "from input data."));
}
void BordersData::DumpPolyFiles(std::string const & targetDir)
{
size_t n = m_bordersPolygons.size();
for (size_t i = 0; i < n;)
{
// Russia_Moscow.poly1 -> Russia_Moscow.poly
auto name = RemoveIndexFromMwmName(m_indexToPolyFileName.at(i));
size_t j = i + 1;
while (j < n && name == RemoveIndexFromMwmName(m_indexToPolyFileName.at(j)))
++j;
std::vector<m2::RegionD> regions;
for (; i < j; i++)
{
if (ShouldLog(i, n))
LOG(LINFO, ("Dumping poly files:", i + 1, "/", n));
m2::RegionD region;
for (auto const & markedPoint : m_bordersPolygons[i].m_points)
region.AddPoint(markedPoint.m_point);
regions.emplace_back(std::move(region));
}
CHECK(strings::ReplaceFirst(name, kBorderExtension, ""), (name));
borders::DumpBorderToPolyFile(targetDir, name, regions);
}
}
size_t BordersData::RemoveDuplicatePoints()
{
size_t count = 0;
for (auto & polygon : m_bordersPolygons)
count += RemoveDuplicatingPointImpl(polygon.m_points);
return count;
}
void BordersData::PrintDiff()
{
using Info = std::tuple<double, std::string, size_t, size_t>;
std::set<Info> info;
size_t allNumberBeforeCount = 0;
size_t maxMwmNameLength = 0;
for (size_t i = 0; i < m_bordersPolygons.size(); ++i)
{
auto const & mwmName = m_indexToPolyFileName[i];
auto const all = static_cast<int32_t>(m_bordersPolygons[i].m_points.size());
auto const allBefore = static_cast<int32_t>(m_prevCopy[i].m_points.size());
CHECK_GREATER_OR_EQUAL(allBefore, all, ());
m_removedPointsCount += allBefore - all;
allNumberBeforeCount += allBefore;
double area = 0.0;
double constexpr kAreaEpsMetersSqr = 1e-4;
if (m_additionalAreaMetersSqr[i] >= kAreaEpsMetersSqr)
area = m_additionalAreaMetersSqr[i];
maxMwmNameLength = std::max(maxMwmNameLength, mwmName.size());
info.emplace(area, mwmName, allBefore, all);
}
for (auto const & [area, name, allBefore, all] : info)
{
size_t diff = allBefore - all;
PrintWithSpaces(name, maxMwmNameLength + 1);
PrintWithSpaces("-" + std::to_string(diff) + " points", 17);
std::cout << " total changed area: " << area << " m^2" << std::endl;
}
CHECK_NOT_EQUAL(allNumberBeforeCount, 0, ("Empty input?"));
std::cout << "Number of removed points: " << m_removedPointsCount << std::endl
<< "Removed duplicate point: " << m_duplicatedPointsCount << std::endl
<< "Total removed points: " << m_removedPointsCount + m_duplicatedPointsCount << std::endl;
std::cout << "Points number before processing: " << allNumberBeforeCount << ", remove( "
<< static_cast<double>(m_removedPointsCount + m_duplicatedPointsCount) / allNumberBeforeCount * 100.0
<< "% )" << std::endl;
}
void BordersData::RemoveEmptySpaceBetweenBorders()
{
LOG(LINFO, ("Start removing empty space between borders."));
for (size_t curBorderId = 0; curBorderId < m_bordersPolygons.size(); ++curBorderId)
{
LOG(LDEBUG, ("Get:", m_indexToPolyFileName[curBorderId]));
if (ShouldLog(curBorderId, m_bordersPolygons.size()))
LOG(LINFO, ("Removing empty spaces:", curBorderId + 1, "/", m_bordersPolygons.size()));
auto & curPolygon = m_bordersPolygons[curBorderId];
for (size_t curPointId = 0; curPointId < curPolygon.m_points.size(); ++curPointId)
{
if (curPolygon.IsFrozen(curPointId, curPointId) || !HasLinkAt(curBorderId, curPointId))
continue;
size_t constexpr kMaxLookAhead = 5;
for (size_t shift = 1; shift <= kMaxLookAhead; ++shift)
{
if (TryToReplace(curBorderId, curPointId /* curLeftPointId */, curPointId + shift /* curRightPointId */) ==
base::ControlFlow::Break)
{
break;
}
}
}
}
DoReplace();
}
base::ControlFlow BordersData::TryToReplace(size_t curBorderId, size_t & curLeftPointId, size_t curRightPointId)
{
auto & curPolygon = m_bordersPolygons[curBorderId];
if (curRightPointId >= curPolygon.m_points.size())
return base::ControlFlow::Break;
if (curPolygon.IsFrozen(curRightPointId, curRightPointId))
{
curLeftPointId = curRightPointId - 1;
return base::ControlFlow::Break;
}
auto & leftMarkedPoint = curPolygon.m_points[curLeftPointId];
auto & rightMarkedPoint = curPolygon.m_points[curRightPointId];
auto op = rightMarkedPoint.GetLink(curBorderId);
if (!op)
return base::ControlFlow::Continue;
Link rightLink = *op;
Link leftLink = *leftMarkedPoint.GetLink(curBorderId);
if (leftLink.m_borderId != rightLink.m_borderId)
return base::ControlFlow::Continue;
auto const anotherBorderId = leftLink.m_borderId;
auto const anotherLeftPointId = leftLink.m_pointId;
auto const anotherRightPointId = rightLink.m_pointId;
auto & anotherPolygon = m_bordersPolygons[anotherBorderId];
if (anotherPolygon.IsFrozen(std::min(anotherLeftPointId, anotherRightPointId),
std::max(anotherLeftPointId, anotherRightPointId)))
{
return base::ControlFlow::Continue;
}
auto const anotherSubpolygon =
AppendPointsWithAnyDirection(anotherPolygon.m_points, anotherLeftPointId, anotherRightPointId);
auto const curSubpolygon = AppendPointsWithAnyDirection(curPolygon.m_points, curLeftPointId, curRightPointId);
if (!NeedReplace(curSubpolygon, anotherSubpolygon))
return base::ControlFlow::Break;
// We want to decrease the amount of points in polygons. So we will replace the greater amounts of
// points by smaller amounts of points.
bool const curLenIsLess = curSubpolygon.size() < anotherSubpolygon.size();
size_t dstFrom = curLenIsLess ? anotherLeftPointId : curLeftPointId;
size_t dstTo = curLenIsLess ? anotherRightPointId : curRightPointId;
size_t srcFrom = curLenIsLess ? curLeftPointId : anotherLeftPointId;
size_t srcTo = curLenIsLess ? curRightPointId : anotherRightPointId;
size_t const borderIdWhereAreaWillBeChanged = curLenIsLess ? anotherBorderId : curBorderId;
size_t const srcBorderId = curLenIsLess ? curBorderId : anotherBorderId;
bool const reversed = IsReversedIntervals(dstFrom, dstTo, srcFrom, srcTo);
m_additionalAreaMetersSqr[borderIdWhereAreaWillBeChanged] += AbsAreaDiff(curSubpolygon, anotherSubpolygon);
SwapIfNeeded(dstFrom, dstTo);
SwapIfNeeded(srcFrom, srcTo);
// Save info for |borderIdWhereAreaWillBeChanged| - where from it should gets info about
// replacement.
m_bordersPolygons[borderIdWhereAreaWillBeChanged].AddReplaceInfo(dstFrom, dstTo, srcFrom, srcTo, srcBorderId,
reversed);
// And say for |srcBorderId| that points in segment: [srcFrom, srcTo] are frozen and cannot
// be used anywhere (because we use them to replace points in segment: [dstFrom, dstTo]).
m_bordersPolygons[srcBorderId].MakeFrozen(srcFrom, srcTo);
CHECK_LESS(curLeftPointId, curRightPointId, ());
curLeftPointId = curRightPointId - 1;
return base::ControlFlow::Break;
}
void BordersData::DoReplace()
{
LOG(LINFO, ("Start replacing intervals."));
std::vector<Polygon> newMwmsPolygons;
for (size_t borderId = 0; borderId < m_bordersPolygons.size(); ++borderId)
{
if (ShouldLog(borderId, m_bordersPolygons.size()))
LOG(LINFO, ("Replacing:", borderId + 1, "/", m_bordersPolygons.size()));
auto & polygon = m_bordersPolygons[borderId];
newMwmsPolygons.emplace_back();
auto & newPolygon = newMwmsPolygons.back();
for (size_t i = 0; i < polygon.m_points.size(); ++i)
{
auto const replaceDataIter = polygon.FindReplaceData(i);
bool const noReplaceData = replaceDataIter == polygon.m_replaceData.cend();
if (noReplaceData)
{
newPolygon.m_points.emplace_back(polygon.m_points[i].m_point);
continue;
}
auto const srcBorderId = replaceDataIter->m_srcBorderId;
size_t const srcFrom = replaceDataIter->m_srcReplaceFrom;
size_t const srcTo = replaceDataIter->m_srcReplaceTo;
size_t const nextI = replaceDataIter->m_dstTo;
bool const reversed = replaceDataIter->m_reversed;
CHECK_EQUAL(i, replaceDataIter->m_dstFrom, ());
auto const & srcPolygon = m_bordersPolygons[srcBorderId];
Append(srcFrom, srcTo, reversed, srcPolygon.m_points, newPolygon.m_points);
polygon.m_replaceData.erase(replaceDataIter);
i = nextI - 1;
}
newPolygon.m_rect = BoundingBox(newPolygon.m_points);
}
m_prevCopy = std::move(m_bordersPolygons);
m_bordersPolygons = std::move(newMwmsPolygons);
RemoveDuplicatePoints();
}
Polygon const & BordersData::GetBordersPolygonByName(std::string const & name) const
{
auto id = m_polyFileNameToIndex.at(name);
return m_bordersPolygons.at(id);
}
bool BordersData::HasLinkAt(size_t curBorderId, size_t pointId)
{
auto & leftMarkedPoint = m_bordersPolygons[curBorderId].m_points[pointId];
return leftMarkedPoint.GetLink(curBorderId).has_value();
}
} // namespace poly_borders

View file

@ -0,0 +1,71 @@
#pragma once
#include "poly_borders/help_structures.hpp"
#include "base/control_flow.hpp"
#include <map>
#include <string>
#include <vector>
namespace poly_borders
{
/// \note All methods of this class are not thread-safe except |MarkPoint()| method.
class BordersData
{
public:
inline static double const kEqualityEpsilon = 1.0E-7;
inline static std::string const kBorderExtension = ".poly";
void Init(std::string const & bordersDir);
void RemoveEmptySpaceBetweenBorders();
void DumpPolyFiles(std::string const & targetDir);
Polygon const & GetBordersPolygonByName(std::string const & name) const;
void PrintDiff();
private:
/// \brief Some polygons can have sequentially same points - duplicates. This method removes such
/// points and leaves only unique.
size_t RemoveDuplicatePoints();
template <class PointsT>
static size_t RemoveDuplicatingPointImpl(PointsT & points)
{
auto const equalFn = [](auto const & p1, auto const & p2) { return p1.EqualDxDy(p2, kEqualityEpsilon); };
auto const last = std::unique(points.begin(), points.end(), equalFn);
size_t count = std::distance(last, points.end());
points.erase(last, points.end());
while (points.size() > 1 && equalFn(points.front(), points.back()))
{
++count;
points.pop_back();
}
return count;
}
/// \brief Checks whether we can replace points from segment: [curLeftPointId, curRightPointId]
/// of |curBorderId| to points from another border in order to get rid of empty space
/// between curBorder and anotherBorder.
base::ControlFlow TryToReplace(size_t curBorderId, size_t & curLeftPointId, size_t curRightPointId);
bool HasLinkAt(size_t curBorderId, size_t pointId);
/// \brief Replace points using |Polygon::ReplaceData| that is filled by
/// |RemoveEmptySpaceBetweenBorders()|.
void DoReplace();
size_t m_removedPointsCount = 0;
size_t m_duplicatedPointsCount = 0;
std::map<size_t, double> m_additionalAreaMetersSqr;
std::map<std::string, size_t> m_polyFileNameToIndex;
std::map<size_t, std::string> m_indexToPolyFileName;
std::vector<Polygon> m_bordersPolygons;
std::vector<Polygon> m_prevCopy;
};
} // namespace poly_borders

View file

@ -0,0 +1,80 @@
#include "poly_borders/help_structures.hpp"
#include <algorithm>
#include <cmath>
#include <tuple>
namespace poly_borders
{
// Link --------------------------------------------------------------------------------------------
bool Link::operator<(Link const & rhs) const
{
return std::tie(m_borderId, m_pointId) < std::tie(rhs.m_borderId, rhs.m_pointId);
}
// ReplaceData -------------------------------------------------------------------------------------
bool ReplaceData::operator<(ReplaceData const & rhs) const
{
return std::tie(m_dstFrom, m_dstTo) < std::tie(rhs.m_dstFrom, rhs.m_dstTo);
}
// MarkedPoint -------------------------------------------------------------------------------------
void MarkedPoint::AddLink(size_t borderId, size_t pointId)
{
m_links.emplace(borderId, pointId);
}
std::optional<Link> MarkedPoint::GetLink(size_t curBorderId) const
{
if (m_links.size() != 1)
return std::nullopt;
size_t anotherBorderId = m_links.begin()->m_borderId;
if (anotherBorderId == curBorderId)
return std::nullopt;
return *m_links.begin();
}
// Polygon -----------------------------------------------------------------------------------------
void Polygon::MakeFrozen(size_t a, size_t b)
{
CHECK_LESS(a, b, ());
// Ends of intervals shouldn't be frozen, we freeze only inner points: (a, b).
// This condition is needed to drop such cases: a = x, b = x + 1, when
// a + 1 will be greater than b - 1.
if (b - a + 1 > 2)
m_replaced.AddInterval(a + 1, b - 1);
}
bool Polygon::IsFrozen(size_t a, size_t b) const
{
// We use LESS_OR_EQUAL because we want sometimes to check if
// point i (associated with interval: [i, i]) is frozen.
CHECK_LESS_OR_EQUAL(a, b, ());
return m_replaced.Intersects(a, b);
}
void Polygon::AddReplaceInfo(size_t dstFrom, size_t dstTo, size_t srcFrom, size_t srcTo, size_t srcBorderId,
bool reversed)
{
CHECK_LESS_OR_EQUAL(dstFrom, dstTo, ());
CHECK_LESS(srcFrom, srcTo, ());
CHECK(!IsFrozen(dstFrom, dstTo), ());
MakeFrozen(dstFrom, dstTo);
m_replaceData.emplace(dstFrom, dstTo, srcFrom, srcTo, srcBorderId, reversed);
}
std::set<ReplaceData>::const_iterator Polygon::FindReplaceData(size_t index)
{
for (auto it = m_replaceData.cbegin(); it != m_replaceData.cend(); ++it)
if (it->m_dstFrom <= index && index <= it->m_dstTo)
return it;
return m_replaceData.cend();
}
} // namespace poly_borders

View file

@ -0,0 +1,101 @@
#pragma once
#include "geometry/rect2d.hpp"
#include "base/non_intersecting_intervals.hpp"
#include <limits>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>
namespace poly_borders
{
struct Link
{
inline static auto constexpr kInvalidId = std::numeric_limits<size_t>::max();
Link() = default;
Link(size_t borderId, size_t pointId) : m_borderId(borderId), m_pointId(pointId) {}
bool operator<(Link const & rhs) const;
size_t m_borderId = kInvalidId;
size_t m_pointId = kInvalidId;
};
/// \note Using next semantic here: [replaceFrom, replaceTo], [replaceFromSrc, replaceToSrc].
struct ReplaceData
{
ReplaceData(size_t replaceFrom, size_t replaceTo, size_t replaceFromSrc, size_t replaceToSrc, size_t borderIdSrc,
bool reversed)
: m_dstFrom(replaceFrom)
, m_dstTo(replaceTo)
, m_srcReplaceFrom(replaceFromSrc)
, m_srcReplaceTo(replaceToSrc)
, m_srcBorderId(borderIdSrc)
, m_reversed(reversed)
{}
bool operator<(ReplaceData const & rhs) const;
size_t m_dstFrom;
size_t m_dstTo;
size_t m_srcReplaceFrom;
size_t m_srcReplaceTo;
size_t m_srcBorderId;
// If |m_srcReplaceFrom| was greater than |m_srcReplaceTo|.
bool m_reversed;
};
struct MarkedPoint
{
MarkedPoint() = default;
MarkedPoint(m2::PointD const & point) : m_point(point) {}
void AddLink(size_t borderId, size_t pointId);
std::optional<Link> GetLink(size_t curBorderId) const;
bool EqualDxDy(MarkedPoint const & p, double eps) const { return m_point.EqualDxDy(p.m_point, eps); }
m2::PointD m_point;
std::set<Link> m_links;
};
struct Polygon
{
Polygon() = default;
Polygon(m2::RectD const & rect, std::vector<m2::PointD> const & points) : m_rect(rect)
{
m_points.assign(points.begin(), points.end());
}
Polygon(m2::RectD const & rect, std::vector<MarkedPoint> && points) : m_rect(rect), m_points(std::move(points)) {}
Polygon(Polygon &&) = default;
Polygon & operator=(Polygon &&) noexcept = default;
// [a, b]
// @{
void MakeFrozen(size_t a, size_t b);
bool IsFrozen(size_t a, size_t b) const;
// @}
// [replaceFrom, replaceTo], [replaceFromSrc, replaceToSrc]
void AddReplaceInfo(size_t replaceFrom, size_t replaceTo, size_t replaceFromSrc, size_t replaceToSrc,
size_t borderIdSrc, bool reversed);
std::set<ReplaceData>::const_iterator FindReplaceData(size_t index);
m2::RectD m_rect;
std::vector<MarkedPoint> m_points;
base::NonIntersectingIntervals<size_t> m_replaced;
std::set<ReplaceData> m_replaceData;
};
} // namespace poly_borders

View file

@ -0,0 +1,15 @@
project(poly_borders_tests)
set(SRC
mark_points_tests.cpp
remove_empty_spaces_tests.cpp
tools.cpp
tools.hpp
)
omim_add_test(${PROJECT_NAME} ${SRC})
target_link_libraries(${PROJECT_NAME}
poly_borders
platform_tests_support
)

View file

@ -0,0 +1,190 @@
#include "poly_borders/poly_borders_tests/tools.hpp"
#include "testing/testing.hpp"
#include "poly_borders/borders_data.hpp"
#include "platform/platform_tests_support/scoped_dir.hpp"
#include "platform/platform_tests_support/scoped_file.hpp"
#include "platform/platform_tests_support/writable_dir_changer.hpp"
#include "platform/platform.hpp"
#include "base/assert.hpp"
#include <memory>
#include <string>
#include <vector>
using namespace platform::tests_support;
using namespace platform;
using namespace poly_borders;
using namespace std;
namespace
{
static string const kTestDir = "borders_poly_dir";
void TestMarked(Polygon const & polygon, size_t i)
{
TEST(!polygon.m_points[i].m_links.empty(), (i, "th point point must be marked."));
}
void TestNotMarked(Polygon const & polygon, size_t i)
{
TEST(polygon.m_points[i].m_links.empty(), (i, "th point must not be marked."));
}
void CheckByMask(Polygon const & polygons, vector<bool> markedMask)
{
CHECK_EQUAL(polygons.m_points.size(), markedMask.size(), ());
for (size_t i = 0; i < polygons.m_points.size(); ++i)
if (markedMask[i])
TestMarked(polygons, i);
else
TestNotMarked(polygons, i);
}
UNIT_TEST(PolyBordersPostprocessor_MarkPoints_1)
{
ScopedDir const scopedDir(kTestDir);
string const & bordersDir = scopedDir.GetFullPath();
m2::PointD a(-1.0, -1.0);
m2::PointD b(-1.0, 1.0);
vector<vector<m2::PointD>> polygons1 = {{a, b, {1.0, 1.0}, {1.0, -1.0}}};
vector<vector<bool>> markedMask1 = {{true, true, false, false}};
vector<vector<m2::PointD>> polygons2 = {{a, b, {2.0, 1.0}, {5.0, -1.0}}};
vector<vector<bool>> markedMask2 = {{true, true, false, false}};
vector<shared_ptr<ScopedFile>> files;
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "First", polygons1));
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "Second", polygons2));
BordersData bordersData;
bordersData.Init(bordersDir);
auto const & bordersPolygon1 = bordersData.GetBordersPolygonByName("First" + BordersData::kBorderExtension + "1");
CheckByMask(bordersPolygon1, markedMask1[0]);
auto const & bordersPolygon2 = bordersData.GetBordersPolygonByName("Second" + BordersData::kBorderExtension + "1");
CheckByMask(bordersPolygon2, markedMask2[0]);
}
UNIT_TEST(PolyBordersPostprocessor_MarkPoints_2)
{
ScopedDir const scopedDir(kTestDir);
string const & bordersDir = scopedDir.GetFullPath();
vector<vector<m2::PointD>> polygons1 = {{{-1.0, -1.0}, {-1.0, 1.0}, {1.0, 1.0}, {1.0, -1.0}}};
vector<vector<bool>> markedMask1 = {{false, false, false, false}};
vector<vector<m2::PointD>> polygons2 = {{{-12.0, -1.0}, {-10.0, 1.0}, {2.0, 1.0}, {5.0, -1.0}}};
vector<vector<bool>> markedMask2 = {{false, false, false, false}};
vector<shared_ptr<ScopedFile>> files;
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "First", polygons1));
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "Second", polygons2));
BordersData bordersData;
bordersData.Init(bordersDir);
auto const & bordersPolygon1 = bordersData.GetBordersPolygonByName("First" + BordersData::kBorderExtension + "1");
CheckByMask(bordersPolygon1, markedMask1[0]);
auto const & bordersPolygon2 = bordersData.GetBordersPolygonByName("Second" + BordersData::kBorderExtension + "1");
CheckByMask(bordersPolygon2, markedMask2[0]);
}
UNIT_TEST(PolyBordersPostprocessor_MarkPoints_3)
{
ScopedDir const scopedDir(kTestDir);
string const & bordersDir = scopedDir.GetFullPath();
m2::PointD a(-2.0, 1.0);
m2::PointD b(0.0, 3.0);
m2::PointD c(3.0, -1.0);
m2::PointD d(-1.0, -3.0);
m2::PointD e(-4.0, 2.0);
m2::PointD f(-1.0, 4.0);
vector<vector<m2::PointD>> polygons1 = {{a, b, c, {1.0, -3.0}, d}};
vector<vector<bool>> markedMask1 = {{true, true, true, false, true}};
vector<vector<m2::PointD>> polygons2 = {{b, f, {2.0, 5.0}, {6.0, 3.0}, c}};
vector<vector<bool>> markedMask2 = {{true, true, false, false, true}};
vector<vector<m2::PointD>> polygons3 = {{a, b, f, {-3.0, 4.0}, e}};
vector<vector<bool>> markedMask3 = {{true, true, true, false, true}};
vector<vector<m2::PointD>> polygons4 = {{a, e, {-3.0, -1.0}, d}};
vector<vector<bool>> markedMask4 = {{true, true, false, true}};
vector<shared_ptr<ScopedFile>> files;
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "First", polygons1));
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "Second", polygons2));
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "Third", polygons3));
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "Fourth", polygons4));
BordersData bordersData;
bordersData.Init(bordersDir);
auto const & bordersPolygon1 = bordersData.GetBordersPolygonByName("First" + BordersData::kBorderExtension + "1");
CheckByMask(bordersPolygon1, markedMask1[0]);
auto const & bordersPolygon2 = bordersData.GetBordersPolygonByName("Second" + BordersData::kBorderExtension + "1");
CheckByMask(bordersPolygon2, markedMask2[0]);
auto const & bordersPolygon3 = bordersData.GetBordersPolygonByName("Third" + BordersData::kBorderExtension + "1");
CheckByMask(bordersPolygon3, markedMask3[0]);
auto const & bordersPolygon4 = bordersData.GetBordersPolygonByName("Fourth" + BordersData::kBorderExtension + "1");
CheckByMask(bordersPolygon4, markedMask4[0]);
}
UNIT_TEST(PolyBordersPostprocessor_MarkPoints_4)
{
ScopedDir const scopedDir(kTestDir);
string const & bordersDir = scopedDir.GetFullPath();
m2::PointD a(6.0, 2.0);
m2::PointD b(6.0, 4.0);
vector<vector<m2::PointD>> polygons1 = {{{-2.0, -2.0}, {-2.0, 2.0}, {2.0, 2.0}, {2.0, -2.0}},
{{4.0, 2.0}, {4.0, 4.0}, a, b}};
vector<vector<bool>> markedMask1 = {{false, false, false, false}, {false, false, true, true}};
vector<vector<m2::PointD>> polygons2 = {{a, b, {8.0, 6.0}, {8.0, 0.0}}};
vector<vector<bool>> markedMask2 = {{true, true, false, false}};
vector<shared_ptr<ScopedFile>> files;
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "First", polygons1));
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "Second", polygons2));
BordersData bordersData;
bordersData.Init(bordersDir);
auto const & firstBordersPolygon1 =
bordersData.GetBordersPolygonByName("First" + BordersData::kBorderExtension + "1");
CheckByMask(firstBordersPolygon1, markedMask1[0]);
auto const & secondBordersPolygon1 =
bordersData.GetBordersPolygonByName("First" + BordersData::kBorderExtension + "2");
CheckByMask(secondBordersPolygon1, markedMask1[1]);
auto const & bordersPolygon2 = bordersData.GetBordersPolygonByName("Second" + BordersData::kBorderExtension + "1");
CheckByMask(bordersPolygon2, markedMask2[0]);
}
} // namespace

View file

@ -0,0 +1,243 @@
#include "poly_borders/poly_borders_tests/tools.hpp"
#include "testing/testing.hpp"
#include "poly_borders/borders_data.hpp"
#include "platform/platform_tests_support/scoped_dir.hpp"
#include "platform/platform_tests_support/scoped_file.hpp"
#include "geometry/point2d.hpp"
#include <set>
#include <string>
#include <vector>
namespace remove_empty_spaces_tests
{
using namespace platform::tests_support;
using namespace platform;
using namespace poly_borders;
using namespace std;
string const kTestDir = "borders_poly_dir";
auto constexpr kSmallShift = 1e-9;
auto constexpr kSmallPointShift = m2::PointD(kSmallShift, kSmallShift);
void Process(BordersData & bordersData, string const & bordersDir)
{
bordersData.Init(bordersDir);
bordersData.RemoveEmptySpaceBetweenBorders();
}
bool ConsistsOf(Polygon const & polygon, vector<m2::PointD> const & points)
{
CHECK_EQUAL(polygon.m_points.size(), points.size(), ());
set<size_t> used;
for (auto const & point : points)
{
for (size_t i = 0; i < polygon.m_points.size(); ++i)
{
static double constexpr kEps = 1e-5;
if (AlmostEqualAbs(point, polygon.m_points[i].m_point, kEps) && used.count(i) == 0)
{
used.emplace(i);
break;
}
}
}
return used.size() == points.size();
}
UNIT_TEST(PolyBordersPostprocessor_RemoveEmptySpaces_1)
{
ScopedDir const scopedDir(kTestDir);
string const & bordersDir = scopedDir.GetFullPath();
m2::PointD a(0.0, 0.0);
m2::PointD b(1.0, 0.0);
m2::PointD c(2.0, 0.0);
m2::PointD d(3.0, 0.0);
m2::PointD e(4.0, 0.0);
vector<vector<m2::PointD>> polygons1 = {{a, b, c, d, e}};
vector<vector<m2::PointD>> polygons2 = {{a, b, c, d, e}};
vector<shared_ptr<ScopedFile>> files;
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "First", polygons1));
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "Second", polygons2));
BordersData bordersData;
Process(bordersData, bordersDir);
auto const & bordersPolygon1 = bordersData.GetBordersPolygonByName("First" + BordersData::kBorderExtension + "1");
TEST(ConsistsOf(bordersPolygon1, {a, b, c, d, e}), ());
auto const & bordersPolygon2 = bordersData.GetBordersPolygonByName("Second" + BordersData::kBorderExtension + "1");
TEST(ConsistsOf(bordersPolygon2, {a, b, c, d, e}), ());
}
UNIT_TEST(PolyBordersPostprocessor_RemoveEmptySpaces_2)
{
ScopedDir const scopedDir(kTestDir);
string const & bordersDir = scopedDir.GetFullPath();
m2::PointD a(0.0, 0.0);
m2::PointD b(1.0, 0.0);
// We should make c.y small because in other case changed area
// will be so great, that point |c| will not be removed.
m2::PointD c(2.0, kSmallShift);
m2::PointD d(3.0, 0.0);
m2::PointD e(4.0, 0.0);
// Point |c| is absent from polygons2, algorithm should remove |c| from polygon1.
vector<vector<m2::PointD>> polygons1 = {{a, b, c, d, e}};
vector<vector<m2::PointD>> polygons2 = {{a, b, d, e}};
vector<shared_ptr<ScopedFile>> files;
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "First", polygons1));
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "Second", polygons2));
BordersData bordersData;
Process(bordersData, bordersDir);
auto const & bordersPolygon1 = bordersData.GetBordersPolygonByName("First" + BordersData::kBorderExtension + "1");
TEST(ConsistsOf(bordersPolygon1, {a, b, d, e}), ());
auto const & bordersPolygon2 = bordersData.GetBordersPolygonByName("Second" + BordersData::kBorderExtension + "1");
TEST(ConsistsOf(bordersPolygon2, {a, b, d, e}), ());
}
// Like |PolyBordersPostprocessor_RemoveEmptySpaces_2| but two points will be
// added instead of one.
UNIT_TEST(PolyBordersPostprocessor_RemoveEmptySpaces_3)
{
ScopedDir const scopedDir(kTestDir);
string const & bordersDir = scopedDir.GetFullPath();
m2::PointD a(0.0, 0.0);
m2::PointD b(1.0, 0.0);
// We should make c.y (and d.y) small because in other case changed area
// will be so great, that point |c| (|d|) will not be removed.
m2::PointD c(2.0, kSmallShift);
m2::PointD d(2.5, kSmallShift);
m2::PointD e(4.0, 0.0);
m2::PointD f(5.0, 0.0);
// Point |c| and |d| is absent from polygons2, algorithm should remove |c| from polygon1.
vector<vector<m2::PointD>> polygons1 = {{a, b, c, d, e, f}};
vector<vector<m2::PointD>> polygons2 = {{a, b, e, f}};
vector<shared_ptr<ScopedFile>> files;
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "First", polygons1));
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "Second", polygons2));
BordersData bordersData;
Process(bordersData, bordersDir);
auto const & bordersPolygon1 = bordersData.GetBordersPolygonByName("First" + BordersData::kBorderExtension + "1");
TEST(ConsistsOf(bordersPolygon1, {a, b, e, f}), ());
auto const & bordersPolygon2 = bordersData.GetBordersPolygonByName("Second" + BordersData::kBorderExtension + "1");
TEST(ConsistsOf(bordersPolygon2, {a, b, e, f}), ());
}
// Do not remove point |c| because changed area is too big.
UNIT_TEST(PolyBordersPostprocessor_RemoveEmptySpaces_4)
{
ScopedDir const scopedDir(kTestDir);
string const & bordersDir = scopedDir.GetFullPath();
m2::PointD a(0.0, 0.0);
m2::PointD b(1.0, 0.0);
m2::PointD c(2.0, 1.0);
m2::PointD d(4.0, 0.0);
m2::PointD e(5.0, 0.0);
vector<vector<m2::PointD>> polygons1 = {{a, b, c, d, e}};
vector<vector<m2::PointD>> polygons2 = {{a, b, d, e}};
vector<shared_ptr<ScopedFile>> files;
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "First", polygons1));
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "Second", polygons2));
BordersData bordersData;
Process(bordersData, bordersDir);
auto const & bordersPolygon1 = bordersData.GetBordersPolygonByName("First" + BordersData::kBorderExtension + "1");
TEST(ConsistsOf(bordersPolygon1, {a, b, c, d, e}), ());
auto const & bordersPolygon2 = bordersData.GetBordersPolygonByName("Second" + BordersData::kBorderExtension + "1");
TEST(ConsistsOf(bordersPolygon2, {a, b, d, e}), ());
}
// Replace {c1, d1, e1} -> {c2, d2}.
UNIT_TEST(PolyBordersPostprocessor_RemoveEmptySpaces_5)
{
ScopedDir const scopedDir(kTestDir);
string const & bordersDir = scopedDir.GetFullPath();
m2::PointD a(0.0, 0.0);
m2::PointD b(9.0, 0.0);
m2::PointD c1(2.0, 3.0);
m2::PointD d1(4.0, 4.0);
m2::PointD e1(d1 + kSmallPointShift + kSmallPointShift);
m2::PointD c2(c1 + kSmallPointShift);
m2::PointD d2(d1 + kSmallPointShift);
vector<vector<m2::PointD>> polygons1 = {{a, c1, d1, e1, b}};
vector<vector<m2::PointD>> polygons2 = {{a, c2, d2, b}};
vector<shared_ptr<ScopedFile>> files;
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "First", polygons1));
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "Second", polygons2));
BordersData bordersData;
Process(bordersData, bordersDir);
auto const & bordersPolygon1 = bordersData.GetBordersPolygonByName("First" + BordersData::kBorderExtension + "1");
TEST(ConsistsOf(bordersPolygon1, {a, c2, d2, b}), ());
auto const & bordersPolygon2 = bordersData.GetBordersPolygonByName("Second" + BordersData::kBorderExtension + "1");
TEST(ConsistsOf(bordersPolygon2, {a, c2, d2, b}), ());
}
// Removes duplicates.
UNIT_TEST(PolyBordersPostprocessor_RemoveEmptySpaces_6)
{
ScopedDir const scopedDir(kTestDir);
string const & bordersDir = scopedDir.GetFullPath();
m2::PointD a(0.0, 0.0);
m2::PointD b(1.0, 0.0);
m2::PointD c(2.0, 1.0);
m2::PointD d(4.0, 0.0);
m2::PointD e(5.0, 0.0);
vector<vector<m2::PointD>> polygons1 = {{a, b, c, d, d, d, e, e, e}};
vector<vector<m2::PointD>> polygons2 = {{a, d, d, d, e}};
vector<shared_ptr<ScopedFile>> files;
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "First", polygons1));
files.emplace_back(CreatePolyBorderFileByPolygon(kTestDir, "Second", polygons2));
BordersData bordersData;
Process(bordersData, bordersDir);
auto const & bordersPolygon1 = bordersData.GetBordersPolygonByName("First" + BordersData::kBorderExtension + "1");
TEST(ConsistsOf(bordersPolygon1, {a, b, c, d, e}), ());
auto const & bordersPolygon2 = bordersData.GetBordersPolygonByName("Second" + BordersData::kBorderExtension + "1");
TEST(ConsistsOf(bordersPolygon2, {a, d, e}), ());
}
} // namespace remove_empty_spaces_tests

View file

@ -0,0 +1,45 @@
#include "poly_borders/poly_borders_tests/tools.hpp"
#include "poly_borders/borders_data.hpp"
#include "generator/borders.hpp"
#include "geometry/region2d.hpp"
#include "base/file_name_utils.hpp"
#include <string>
#include <vector>
using namespace platform::tests_support;
namespace
{
std::vector<m2::RegionD> ConvertFromPointsVector(std::vector<std::vector<m2::PointD>> const & polygons)
{
std::vector<m2::RegionD> res;
res.reserve(polygons.size());
for (auto const & polygon : polygons)
res.emplace_back(polygon);
return res;
}
} // namespace
namespace poly_borders
{
std::shared_ptr<ScopedFile> CreatePolyBorderFileByPolygon(std::string const & relativeDirPath, std::string const & name,
std::vector<std::vector<m2::PointD>> const & polygons)
{
std::string path = base::JoinPath(relativeDirPath, name + BordersData::kBorderExtension);
auto file = std::make_shared<ScopedFile>(path, ScopedFile::Mode::Create);
auto const targetDir = base::GetDirectory(file->GetFullPath());
auto const regions = ConvertFromPointsVector(polygons);
borders::DumpBorderToPolyFile(targetDir, name, regions);
return file;
}
} // namespace poly_borders

View file

@ -0,0 +1,16 @@
#pragma once
#include "platform/platform_tests_support/scoped_file.hpp"
#include "geometry/point2d.hpp"
#include <memory>
#include <string>
#include <vector>
namespace poly_borders
{
std::shared_ptr<platform::tests_support::ScopedFile> CreatePolyBorderFileByPolygon(
std::string const & relativeDirPath, std::string const & name,
std::vector<std::vector<m2::PointD>> const & polygons);
} // namespace poly_borders

View file

@ -0,0 +1,10 @@
project(poly_borders_tool)
set(SRC poly_borders_tool.cpp)
omim_add_executable(${PROJECT_NAME} ${SRC})
target_link_libraries(${PROJECT_NAME}
poly_borders
gflags::gflags
)

View file

@ -0,0 +1,64 @@
#include "poly_borders/borders_data.hpp"
#include "poly_borders/help_structures.hpp"
#include "platform/platform.hpp"
#include "base/assert.hpp"
#include "base/exception.hpp"
#include "base/logging.hpp"
#include <exception>
#include <gflags/gflags.h>
DEFINE_string(borders_path, "", "Path to directory with *.poly files.");
DEFINE_string(output_path, "", "Path to target directory where the output *.poly files will be placed.");
using namespace poly_borders;
int main(int argc, char ** argv)
{
gflags::ParseCommandLineFlags(&argc, &argv, true);
gflags::SetUsageMessage(
"\n\n\tThe tool is used to process *.poly borders files. We use such files\n"
"\tto cut the planet into mwms in generator. The problem is that we have\n"
"\tempty spaces between neighbouring borders. This tool creates new borders\n"
"\tbased on input data by removing points from borders in such a way that the\n"
"\tchanged area of each border will not be too large.\n"
"\tArguments:\n"
"\t\t--borders_path=/path/to/directory/with/borders\n"
"\t\t--output_path=/path/to/directory/where/new/borders/will/be/placed\n");
if (FLAGS_borders_path.empty() || FLAGS_output_path.empty())
{
gflags::ShowUsageWithFlags("poly_borders_postprocessor");
return 0;
}
CHECK_NOT_EQUAL(FLAGS_borders_path, FLAGS_output_path, ());
CHECK(Platform::IsDirectory(FLAGS_borders_path), ("Cannot find directory:", FLAGS_borders_path));
CHECK(Platform::IsDirectory(FLAGS_output_path), ("Cannot find directory:", FLAGS_output_path));
try
{
BordersData data;
data.Init(FLAGS_borders_path);
data.RemoveEmptySpaceBetweenBorders();
data.PrintDiff();
data.DumpPolyFiles(FLAGS_output_path);
}
catch (RootException const & e)
{
LOG(LERROR, ("Core exception:", e.Msg()));
}
catch (std::exception const & e)
{
LOG(LERROR, ("Std exception:", e.what()));
}
catch (...)
{
LOG(LERROR, ("Unknown exception."));
}
return 0;
}

2
tools/python/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
osm_cache.json
transit/18*/

View file

@ -0,0 +1,57 @@
#!/usr/bin/env python3
from __future__ import print_function
import struct
import sys
import numpy
class Analyzer:
"""
The binary format is
time since the beginning of the measurement : double
unknown and irrelevant field : double
momentary consumption calculated for the current time segment : double
"""
def __init__(self):
self.duration = 0.0
self.consumption = []
self.mean = 0.0
self.std = 0.0
self.avg = 0.0
self.averages = []
def read_file(self, file_path):
binary = bytearray()
with open(file_path, "r") as f:
binary = bytearray(f.read())
for i in range(0, len(binary) - 24, 24):
res = struct.unpack(">ddd", binary[i:i+24])
current_duration = res[0]
if not current_duration > self.duration:
print("Unexpected elapsed time value, lower than the previous one.")
exit(2) # this should never happen because the file is written sequentially
current_consumption = res[2]
self.averages.append(current_consumption / (current_duration - self.duration))
self.duration = current_duration
self.consumption.append(current_consumption)
self.calculate_stats()
def calculate_stats(self):
self.mean = numpy.mean(self.averages)
self.std = numpy.std(self.averages)
self.avg = sum(self.consumption) / self.duration
if __name__ == "__main__":
for file_path in sys.argv[1:]:
analyzer = Analyzer()
analyzer.read_file(file_path)
print("{}\n\tavg: {}\n\tmean: {}\n\tstd: {}".format(file_path, analyzer.avg, analyzer.mean, analyzer.std))

14
tools/python/Util.py Normal file
View file

@ -0,0 +1,14 @@
from contextlib import contextmanager
import shutil
import tempfile
try:
from tempfile import TemporaryDirectory
except ImportError:
@contextmanager
def TemporaryDirectory():
name = tempfile.mkdtemp()
try:
yield name
finally:
shutil.rmtree(name)

0
tools/python/__init__.py Normal file
View file

View file

@ -0,0 +1,16 @@
# airmaps - building of maps using airflow.
## Storage
Repository of result and temporary files.
Currently, the storage is a webdav server.
## Description of DAGs:
1. Update_planet - updates .o5m planet file.
2. Build_coastline - builds coastline files.
3. Generate_open_source_maps - builds free maps for maps.me
All results will be published on the storage.

View file

@ -0,0 +1,12 @@
import os
from airmaps.instruments import settings
CONFIG_PATH = os.path.join(
os.path.dirname(os.path.join(os.path.realpath(__file__))),
"var",
"etc",
"airmaps.ini",
)
settings.init(CONFIG_PATH)

View file

@ -0,0 +1,93 @@
import logging
import os
import shutil
from datetime import timedelta
from airflow import DAG
from airflow.operators.python_operator import PythonOperator
from airflow.utils.dates import days_ago
from airmaps.instruments import settings
from airmaps.instruments import storage
from airmaps.instruments.utils import get_latest_filename
from airmaps.instruments.utils import make_rm_build_task
from airmaps.instruments.utils import put_current_date_in_filename
from airmaps.instruments.utils import rm_build
from maps_generator.generator import stages_declaration as sd
from maps_generator.generator.env import Env
from maps_generator.generator.env import WORLD_COASTS_NAME
from maps_generator.maps_generator import run_generation
logger = logging.getLogger("airmaps")
DAG = DAG(
"Build_coastline",
schedule_interval=timedelta(days=1),
default_args={
"owner": "OMaps",
"depends_on_past": True,
"start_date": days_ago(0),
"email": settings.EMAILS,
"email_on_failure": True,
"email_on_retry": False,
"retries": 0,
"retry_delay": timedelta(minutes=5),
"priority_weight": 1,
},
)
COASTLINE_STORAGE_PATH = f"{settings.STORAGE_PREFIX}/coasts"
def publish_coastline(**kwargs):
build_name = kwargs["ti"].xcom_pull(key="build_name")
env = Env(build_name=build_name)
for name in (f"{WORLD_COASTS_NAME}.geom", f"{WORLD_COASTS_NAME}.rawgeom"):
coastline = put_current_date_in_filename(name)
latest = get_latest_filename(name)
coastline_full = os.path.join(env.paths.coastline_path, coastline)
latest_full = os.path.join(env.paths.coastline_path, latest)
shutil.move(os.path.join(env.paths.coastline_path, name), coastline_full)
os.symlink(coastline, latest_full)
storage.wd_publish(coastline_full, f"{COASTLINE_STORAGE_PATH}/{coastline}")
storage.wd_publish(latest_full, f"{COASTLINE_STORAGE_PATH}/{latest}")
def build_coastline(**kwargs):
env = Env()
kwargs["ti"].xcom_push(key="build_name", value=env.build_name)
run_generation(
env,
(
sd.StageDownloadAndConvertPlanet(),
sd.StageCoastline(use_old_if_fail=False),
sd.StageCleanup(),
),
)
env.finish()
BUILD_COASTLINE_TASK = PythonOperator(
task_id="Build_coastline_task",
provide_context=True,
python_callable=build_coastline,
on_failure_callback=lambda ctx: rm_build(**ctx),
dag=DAG,
)
PUBLISH_COASTLINE_TASK = PythonOperator(
task_id="Publish_coastline_task",
provide_context=True,
python_callable=publish_coastline,
dag=DAG,
)
RM_BUILD_TASK = make_rm_build_task(DAG)
BUILD_COASTLINE_TASK >> PUBLISH_COASTLINE_TASK >> RM_BUILD_TASK

View file

@ -0,0 +1,154 @@
import logging
from datetime import timedelta
from airflow import DAG
from airflow.operators.python_operator import PythonOperator
from airflow.utils.dates import days_ago
from airmaps.instruments import settings
from airmaps.instruments import storage
from airmaps.instruments.utils import make_rm_build_task
from airmaps.instruments.utils import run_generation_from_first_stage
from maps_generator.generator import stages_declaration as sd
from maps_generator.generator.env import Env
from maps_generator.generator.env import PathProvider
from maps_generator.generator.env import get_all_countries_list
from maps_generator.maps_generator import run_generation
logger = logging.getLogger("airmaps")
MAPS_STORAGE_PATH = f"{settings.STORAGE_PREFIX}/maps"
class MapsGenerationDAG(DAG):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
build_prolog_task = PythonOperator(
task_id="Build_prolog_task",
provide_context=True,
python_callable=MapsGenerationDAG.build_prolog,
dag=self,
)
build_epilog_task = PythonOperator(
task_id="Build_epilog_task",
provide_context=True,
python_callable=MapsGenerationDAG.build_epilog,
dag=self,
)
publish_maps_task = PythonOperator(
task_id="Publish_maps_task",
provide_context=True,
python_callable=MapsGenerationDAG.publish_maps,
dag=self,
)
rm_build_task = make_rm_build_task(self)
build_epilog_task >> publish_maps_task >> rm_build_task
for country in get_all_countries_list(PathProvider.borders_path()):
build_prolog_task >> self.make_mwm_operator(country) >> build_epilog_task
@staticmethod
def get_params(namespace="env", **kwargs):
return kwargs.get("params", {}).get(namespace, {})
@staticmethod
def build_prolog(**kwargs):
params = MapsGenerationDAG.get_params(**kwargs)
env = Env(**params)
kwargs["ti"].xcom_push(key="build_name", value=env.build_name)
run_generation(
env,
(
sd.StageDownloadAndConvertPlanet(),
sd.StageCoastline(),
sd.StagePreprocess(),
sd.StageFeatures(),
sd.StageDownloadDescriptions(),
),
)
@staticmethod
def make_build_mwm_func(country):
def build_mwm(**kwargs):
build_name = kwargs["ti"].xcom_pull(key="build_name")
params = MapsGenerationDAG.get_params(**kwargs)
params.update({"build_name": build_name, "countries": [country,]})
env = Env(**params)
# We need to check existing of mwm.tmp. It is needed if we want to
# build mwms from part of planet.
tmp_mwm_name = env.get_tmp_mwm_names()
assert len(tmp_mwm_name) <= 1
if not tmp_mwm_name:
logger.warning(f"mwm.tmp does not exist for {country}.")
return
run_generation_from_first_stage(env, (sd.StageMwm(),), build_lock=False)
return build_mwm
@staticmethod
def build_epilog(**kwargs):
build_name = kwargs["ti"].xcom_pull(key="build_name")
params = MapsGenerationDAG.get_params(**kwargs)
params.update({"build_name": build_name})
env = Env(**params)
run_generation_from_first_stage(
env,
(
sd.StageCountriesTxt(),
sd.StageLocalAds(),
sd.StageStatistics(),
sd.StageCleanup(),
),
)
env.finish()
@staticmethod
def publish_maps(**kwargs):
build_name = kwargs["ti"].xcom_pull(key="build_name")
params = MapsGenerationDAG.get_params(**kwargs)
params.update({"build_name": build_name})
env = Env(**params)
subdir = MapsGenerationDAG.get_params(namespace="storage", **kwargs)["subdir"]
storage_path = f"{MAPS_STORAGE_PATH}/{subdir}"
storage.wd_publish(env.paths.mwm_path, f"{storage_path}/{env.mwm_version}/")
def make_mwm_operator(self, country):
normalized_name = "__".join(country.lower().split())
return PythonOperator(
task_id=f"Build_country_{normalized_name}_task",
provide_context=True,
python_callable=MapsGenerationDAG.make_build_mwm_func(country),
dag=self,
)
PARAMS = {"storage": {"subdir": "open_source"}}
if settings.DEBUG:
PARAMS["env"] = {
# The planet file in debug mode does not contain Russia_Moscow territory.
# It is needed for testing.
"countries": ["Cuba", "Haiti", "Jamaica", "Cayman Islands", "Russia_Moscow"]
}
OPEN_SOURCE_MAPS_GENERATION_DAG = MapsGenerationDAG(
"Generate_open_source_maps",
schedule_interval=timedelta(days=7),
default_args={
"owner": "OMaps",
"depends_on_past": True,
"start_date": days_ago(0),
"email": settings.EMAILS,
"email_on_failure": True,
"email_on_retry": False,
"retries": 0,
"retry_delay": timedelta(minutes=5),
"priority_weight": 1,
"params": PARAMS,
},
)

View file

@ -0,0 +1,83 @@
import logging
from datetime import timedelta
from airflow import DAG
from airflow.operators.python_operator import PythonOperator
from airflow.utils.dates import days_ago
from airmaps.instruments import settings
from airmaps.instruments import storage
from airmaps.instruments.utils import make_rm_build_task
from maps_generator.generator import stages_declaration as sd
from maps_generator.generator.env import Env
from maps_generator.maps_generator import run_generation
from maps_generator.utils.md5 import md5_ext
logger = logging.getLogger("airmaps")
DAG = DAG(
"Update_planet",
schedule_interval=timedelta(days=1),
default_args={
"owner": "OMaps",
"depends_on_past": True,
"start_date": days_ago(0),
"email": settings.EMAILS,
"email_on_failure": True,
"email_on_retry": False,
"retries": 0,
"retry_delay": timedelta(minutes=5),
"priority_weight": 1,
},
)
PLANET_STORAGE_PATH = f"{settings.STORAGE_PREFIX}/planet_regular/planet-latest.o5m"
def update_planet(**kwargs):
env = Env()
kwargs["ti"].xcom_push(key="build_name", value=env.build_name)
if settings.DEBUG:
env.add_skipped_stage(sd.StageUpdatePlanet)
run_generation(
env,
(
sd.StageDownloadAndConvertPlanet(),
sd.StageUpdatePlanet(),
sd.StageCleanup(),
),
)
env.finish()
def publish_planet(**kwargs):
build_name = kwargs["ti"].xcom_pull(key="build_name")
env = Env(build_name=build_name)
storage.wd_publish(env.paths.planet_o5m, PLANET_STORAGE_PATH)
storage.wd_publish(md5_ext(env.paths.planet_o5m), md5_ext(PLANET_STORAGE_PATH))
UPDATE_PLANET_TASK = PythonOperator(
task_id="Update_planet_task",
provide_context=True,
python_callable=update_planet,
dag=DAG,
)
PUBLISH_PLANET_TASK = PythonOperator(
task_id="Publish_planet_task",
provide_context=True,
python_callable=publish_planet,
dag=DAG,
)
RM_BUILD_TASK = make_rm_build_task(DAG)
UPDATE_PLANET_TASK >> PUBLISH_PLANET_TASK >> RM_BUILD_TASK

View file

@ -0,0 +1,62 @@
import sys
from typing import AnyStr
from maps_generator.generator import settings
STORAGE_PREFIX = ""
# Storage settings
WD_HOST = ""
WD_LOGIN = ""
WD_PASSWORD = ""
# Common section
EMAILS = []
settings.LOGGING["loggers"]["airmaps"] = {
"handlers": ["stdout", "file"],
"level": "DEBUG",
"propagate": True,
}
def get_airmaps_emails(emails: AnyStr):
if not emails:
return []
for ch in [",", ";", ":"]:
emails.replace(ch, " ")
return list(filter(None, [e.strpip() for e in emails.join(" ")]))
def init(default_settings_path: AnyStr):
settings.init(default_settings_path)
# Try to read a config and to overload default settings
cfg = settings.CfgReader(default_settings_path)
# Storage section
global WD_HOST
global WD_LOGIN
global WD_PASSWORD
WD_HOST = cfg.get_opt("Storage", "WD_HOST", WD_HOST)
WD_LOGIN = cfg.get_opt("Storage", "WD_LOGIN", WD_LOGIN)
WD_PASSWORD = cfg.get_opt("Storage", "WD_PASSWORD", WD_PASSWORD)
# Common section
global EMAILS
emails = cfg.get_opt("Common", "EMAILS", "")
EMAILS = get_airmaps_emails(emails)
# Import all contains from maps_generator.generator.settings.
thismodule = sys.modules[__name__]
for name in dir(settings):
if not name.startswith("_") and name.isupper():
value = getattr(settings, name)
setattr(thismodule, name, value)
global STORAGE_PREFIX
if settings.DEBUG:
STORAGE_PREFIX = "/tests"

View file

@ -0,0 +1,27 @@
import logging
import webdav.client as wc
from airmaps.instruments import settings
logger = logging.getLogger("airmaps")
WD_OPTIONS = {
"webdav_hostname": settings.WD_HOST,
"webdav_login": settings.WD_LOGIN,
"webdav_password": settings.WD_PASSWORD,
}
def wd_fetch(src, dst):
logger.info(f"Fetch form {src} to {dst} with options {WD_OPTIONS}.")
client = wc.Client(WD_OPTIONS)
client.download_sync(src, dst)
def wd_publish(src, dst):
logger.info(f"Publish form {src} to {dst} with options {WD_OPTIONS}.")
client = wc.Client(WD_OPTIONS)
tmp = f"{dst[:-1]}__/" if dst[-1] == "/" else f"{dst}__"
client.upload_sync(local_path=src, remote_path=tmp)
client.move(remote_path_from=tmp, remote_path_to=dst)

View file

@ -0,0 +1,48 @@
import os
import shutil
from datetime import datetime
from typing import Iterable
from airflow.operators.python_operator import PythonOperator
from maps_generator.generator.env import Env
from maps_generator.generator.stages import Stage
from maps_generator.generator.stages import get_stage_name
from maps_generator.maps_generator import run_generation
def put_current_date_in_filename(filename):
path, name = os.path.split(filename)
parts = name.split(".", maxsplit=1)
parts[0] += f"__{datetime.today().strftime('%Y_%m_%d')}"
return os.path.join(path, ".".join(parts))
def get_latest_filename(filename, prefix=""):
path, name = os.path.split(filename)
parts = name.split(".", maxsplit=1)
assert len(parts) != 0, parts
parts[0] = f"{prefix}latest"
return os.path.join(path, ".".join(parts))
def rm_build(**kwargs):
build_name = kwargs["ti"].xcom_pull(key="build_name")
env = Env(build_name=build_name)
shutil.rmtree(env.build_path)
def make_rm_build_task(dag):
return PythonOperator(
task_id="Rm_build_task",
provide_context=True,
python_callable=rm_build,
dag=dag,
)
def run_generation_from_first_stage(
env: Env, stages: Iterable[Stage], build_lock: bool = True
):
from_stage = get_stage_name(next(iter(stages)))
run_generation(env, stages, from_stage, build_lock)

View file

@ -0,0 +1,6 @@
omim-data-all
omim-maps_generator
apache-airflow [postgres]==1.10.10
psycopg2-binary==2.8.4
cryptography>=41.0.0
webdavclient==1.0.8

View file

@ -0,0 +1,5 @@
-r ../maps_generator/requirements_dev.txt
apache-airflow [postgres]==1.10.10
psycopg2-binary==2.8.4
cryptography>=41.0.0
webdavclient==1.0.8

View file

@ -0,0 +1,4 @@
./.git*
./android
./iphone
./xcode

View file

@ -0,0 +1,92 @@
# Sandbox
This project can show how airmaps works on your computer.
## Setup
You must have [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
0. Change working directory:
```sh
$ cd omim/tools/python/airmaps/sandbox
```
1. Build airmaps service:
```sh
sandbox$ ./build.sh
```
2. Create storage(sandbox/storage directory):
```sh
sandbox$ ./create_storage.sh
```
Note: May be you need `sudo`, because `./create_storage.sh` to try change an owner of `sandbox/storage/tests` directory.
## Usage
### Starting
0. Change working directory:
```sh
$ cd omim/tools/python/airmaps/sandbox
```
1. Run all services:
```sh
sandbox$ docker-compose up
```
2. Open http://localhost in your browser.
Note: You can see the results of airmaps working in `sandbox/storage/tests`.
### Stopping
0. Change working directory:
```sh
$ cd omim/tools/python/airmaps/sandbox
```
1. Stop all services:
Push Ctrl+C and
```sh
sandbox$ docker-compose down
```
### Clean
#### Clean storage and intermediate files:
0. Change working directory:
```sh
$ cd omim/tools/python/airmaps/sandbox
```
1. Clean storage and intermediate files:
```sh
sandbox$ ./clean.sh
```
#### Remove images:
0. Change working directory:
```sh
$ cd omim/tools/python/airmaps/sandbox
```
1. Remove images:
```sh
sandbox$ docker-compose rm
```

View file

@ -0,0 +1,28 @@
FROM python:3.6
ARG TZ=Etc/UTC
WORKDIR /omim/
ADD . .
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
libgl1-mesa-dev \
libsqlite3-dev \
qt5-default \
zlib1g-dev \
tzdata \
locales-all
RUN ln -fs /usr/share/zoneinfo/$TZ /etc/localtime && \
dpkg-reconfigure --frontend noninteractive tzdata
RUN ./tools/unix/build_omim.sh -rs generator_tool
RUN pip install --upgrade pip
RUN pip install werkzeug==0.16.0 \
SQLAlchemy==1.3.15 \
-r ./tools/python/airmaps/requirements_dev.txt

View file

@ -0,0 +1,856 @@
[core]
# The folder where your airflow pipelines live, most likely a
# subfolder in a code repository
# This path must be absolute
dags_folder = /omim/tools/python/airmaps/dags
# The folder where airflow should store its log files
# This path must be absolute
base_log_folder = /airflow_home/logs
# Airflow can store logs remotely in AWS S3, Google Cloud Storage or Elastic Search.
# Users must supply an Airflow connection id that provides access to the storage
# location. If remote_logging is set to true, see UPDATING.md for additional
# configuration requirements.
remote_logging = False
remote_log_conn_id =
remote_base_log_folder =
encrypt_s3_logs = False
# Logging level
logging_level = INFO
fab_logging_level = WARN
# Logging class
# Specify the class that will specify the logging configuration
# This class has to be on the python classpath
# logging_config_class = my.path.default_local_settings.LOGGING_CONFIG
logging_config_class =
# Log format
# Colour the logs when the controlling terminal is a TTY.
colored_console_log = True
colored_log_format = [%%(blue)s%%(asctime)s%%(reset)s] {%%(blue)s%%(filename)s:%%(reset)s%%(lineno)d} %%(log_color)s%%(levelname)s%%(reset)s - %%(log_color)s%%(message)s%%(reset)s
colored_formatter_class = airflow.utils.log.colored_log.CustomTTYColoredFormatter
log_format = [%%(asctime)s] {%%(filename)s:%%(lineno)d} %%(levelname)s - %%(message)s
simple_log_format = %%(asctime)s %%(levelname)s - %%(message)s
# Log filename format
log_filename_template = {{ ti.dag_id }}/{{ ti.task_id }}/{{ ts }}/{{ try_number }}.log
log_processor_filename_template = {{ filename }}.log
dag_processor_manager_log_location = /airflow_home/logs/dag_processor_manager/dag_processor_manager.log
# Hostname by providing a path to a callable, which will resolve the hostname
# The format is "package:function". For example,
# default value "socket:getfqdn" means that result from getfqdn() of "socket" package will be used as hostname
# No argument should be required in the function specified.
# If using IP address as hostname is preferred, use value "airflow.utils.net:get_host_ip_address"
hostname_callable = socket:getfqdn
# Default timezone in case supplied date times are naive
# can be utc (default), system, or any IANA timezone string (e.g. Europe/Amsterdam)
default_timezone = system
# The executor class that airflow should use. Choices include
# SequentialExecutor, LocalExecutor, CeleryExecutor, DaskExecutor, KubernetesExecutor
executor = LocalExecutor
# The SqlAlchemy connection string to the metadata database.
# SqlAlchemy supports many different database engine, more information
# their website
sql_alchemy_conn = postgresql+psycopg2://postgres:postgres@db:5432/airflow
# The encoding for the databases
sql_engine_encoding = utf-8
# If SqlAlchemy should pool database connections.
sql_alchemy_pool_enabled = True
# The SqlAlchemy pool size is the maximum number of database connections
# in the pool. 0 indicates no limit.
sql_alchemy_pool_size = 5
# The maximum overflow size of the pool.
# When the number of checked-out connections reaches the size set in pool_size,
# additional connections will be returned up to this limit.
# When those additional connections are returned to the pool, they are disconnected and discarded.
# It follows then that the total number of simultaneous connections the pool will allow is pool_size + max_overflow,
# and the total number of "sleeping" connections the pool will allow is pool_size.
# max_overflow can be set to -1 to indicate no overflow limit;
# no limit will be placed on the total number of concurrent connections. Defaults to 10.
sql_alchemy_max_overflow = 10
# The SqlAlchemy pool recycle is the number of seconds a connection
# can be idle in the pool before it is invalidated. This config does
# not apply to sqlite. If the number of DB connections is ever exceeded,
# a lower config value will allow the system to recover faster.
sql_alchemy_pool_recycle = 1800
# Check connection at the start of each connection pool checkout.
# Typically, this is a simple statement like “SELECT 1”.
# More information here: https://docs.sqlalchemy.org/en/13/core/pooling.html#disconnect-handling-pessimistic
sql_alchemy_pool_pre_ping = True
# The schema to use for the metadata database
# SqlAlchemy supports databases with the concept of multiple schemas.
sql_alchemy_schema =
# The amount of parallelism as a setting to the executor. This defines
# the max number of task instances that should run simultaneously
# on this airflow installation
parallelism = 32
# The number of task instances allowed to run concurrently by the scheduler
dag_concurrency = 16
# Are DAGs paused by default at creation
dags_are_paused_at_creation = True
# The maximum number of active DAG runs per DAG
max_active_runs_per_dag = 16
# Whether to load the examples that ship with Airflow. It's good to
# get started, but you probably want to set this to False in a production
# environment
load_examples = False
# Where your Airflow plugins are stored
plugins_folder = /airflow_home/plugins
# Secret key to save connection passwords in the db
fernet_key = uoTKzPCjhVBsERkDylXY5g1hYeg7OAYjk_a_ek2YMwQ=
# Whether to disable pickling dags
donot_pickle = False
# How long before timing out a python file import
dagbag_import_timeout = 30
# How long before timing out a DagFileProcessor, which processes a dag file
dag_file_processor_timeout = 50
# The class to use for running task instances in a subprocess
task_runner = StandardTaskRunner
# If set, tasks without a `run_as_user` argument will be run with this user
# Can be used to de-elevate a sudo user running Airflow when executing tasks
default_impersonation =
# What security module to use (for example kerberos):
security =
# If set to False enables some unsecure features like Charts and Ad Hoc Queries.
# In 2.0 will default to True.
secure_mode = False
# Turn unit test mode on (overwrites many configuration options with test
# values at runtime)
unit_test_mode = False
# Name of handler to read task instance logs.
# Default to use task handler.
task_log_reader = task
# Whether to enable pickling for xcom (note that this is insecure and allows for
# RCE exploits). This will be deprecated in Airflow 2.0 (be forced to False).
enable_xcom_pickling = True
# When a task is killed forcefully, this is the amount of time in seconds that
# it has to cleanup after it is sent a SIGTERM, before it is SIGKILLED
killed_task_cleanup_time = 60
# Whether to override params with dag_run.conf. If you pass some key-value pairs through `airflow backfill -c` or
# `airflow trigger_dag -c`, the key-value pairs will override the existing ones in params.
dag_run_conf_overrides_params = False
# Worker initialisation check to validate Metadata Database connection
worker_precheck = False
# When discovering DAGs, ignore any files that don't contain the strings `DAG` and `airflow`.
dag_discovery_safe_mode = True
# The number of retries each task is going to have by default. Can be overridden at dag or task level.
default_task_retries = 0
[cli]
# In what way should the cli access the API. The LocalClient will use the
# database directly, while the json_client will use the api running on the
# webserver
api_client = airflow.api.client.local_client
# If you set web_server_url_prefix, do NOT forget to append it here, ex:
# endpoint_url = http://localhost:8080/myroot
# So api will look like: http://localhost:8080/myroot/api/experimental/...
endpoint_url = http://localhost:8080
[api]
# How to authenticate users of the API
auth_backend = airflow.api.auth.backend.default
[lineage]
# what lineage backend to use
backend =
[atlas]
sasl_enabled = False
host =
port = 21000
username =
password =
[operators]
# The default owner assigned to each new operator, unless
# provided explicitly or passed via `default_args`
default_owner = airflow
default_cpus = 1
default_ram = 512
default_disk = 512
default_gpus = 0
[hive]
# Default mapreduce queue for HiveOperator tasks
default_hive_mapred_queue =
[webserver]
# The base url of your website as airflow cannot guess what domain or
# cname you are using. This is used in automated emails that
# airflow sends to point links to the right web server
base_url = http://localhost:8080
# Default timezone to display all dates in the UI, can be UTC, system, or
# any IANA timezone string (e.g. Europe/Amsterdam). If left empty the
# default value of core/default_timezone will be used
# Example: default_ui_timezone = America/New_York
default_ui_timezone = system
# The ip specified when starting the web server
web_server_host = 0.0.0.0
# The port on which to run the web server
web_server_port = 8080
# Paths to the SSL certificate and key for the web server. When both are
# provided SSL will be enabled. This does not change the web server port.
web_server_ssl_cert =
web_server_ssl_key =
# Number of seconds the webserver waits before killing gunicorn master that doesn't respond
web_server_master_timeout = 120
# Number of seconds the gunicorn webserver waits before timing out on a worker
web_server_worker_timeout = 120
# Number of workers to refresh at a time. When set to 0, worker refresh is
# disabled. When nonzero, airflow periodically refreshes webserver workers by
# bringing up new ones and killing old ones.
worker_refresh_batch_size = 1
# Number of seconds to wait before refreshing a batch of workers.
worker_refresh_interval = 30
# Secret key used to run your flask app
secret_key = temporary_key
# Number of workers to run the Gunicorn web server
workers = 4
# The worker class gunicorn should use. Choices include
# sync (default), eventlet, gevent
worker_class = sync
# Log files for the gunicorn webserver. '-' means log to stderr.
access_logfile = -
error_logfile = -
# Expose the configuration file in the web server
# This is only applicable for the flask-admin based web UI (non FAB-based).
# In the FAB-based web UI with RBAC feature,
# access to configuration is controlled by role permissions.
expose_config = False
# Set to true to turn on authentication:
# https://airflow.apache.org/security.html#web-authentication
authenticate = False
# Filter the list of dags by owner name (requires authentication to be enabled)
filter_by_owner = False
# Filtering mode. Choices include user (default) and ldapgroup.
# Ldap group filtering requires using the ldap backend
#
# Note that the ldap server needs the "memberOf" overlay to be set up
# in order to user the ldapgroup mode.
owner_mode = user
# Default DAG view. Valid values are:
# tree, graph, duration, gantt, landing_times
dag_default_view = tree
# Default DAG orientation. Valid values are:
# LR (Left->Right), TB (Top->Bottom), RL (Right->Left), BT (Bottom->Top)
dag_orientation = LR
# Puts the webserver in demonstration mode; blurs the names of Operators for
# privacy.
demo_mode = False
# The amount of time (in secs) webserver will wait for initial handshake
# while fetching logs from other worker machine
log_fetch_timeout_sec = 5
# By default, the webserver shows paused DAGs. Flip this to hide paused
# DAGs by default
hide_paused_dags_by_default = False
# Consistent page size across all listing views in the UI
page_size = 100
# Use FAB-based webserver with RBAC feature
rbac = False
# Define the color of navigation bar
navbar_color = #007A87
# Default dagrun to show in UI
default_dag_run_display_number = 25
# Enable werkzeug `ProxyFix` middleware
enable_proxy_fix = False
# Set secure flag on session cookie
cookie_secure = False
# Set samesite policy on session cookie
cookie_samesite =
# Default setting for wrap toggle on DAG code and TI log views.
default_wrap = False
# Send anonymous user activity to your analytics tool
# analytics_tool = # choose from google_analytics, segment, or metarouter
# analytics_id = XXXXXXXXXXX
[email]
email_backend = airflow.utils.email.send_email_smtp
[smtp]
# If you want airflow to send emails on retries, failure, and you want to use
# the airflow.utils.email.send_email_smtp function, you have to configure an
# smtp server here
smtp_host = localhost
smtp_starttls = True
smtp_ssl = False
# Uncomment and set the user/pass settings if you want to use SMTP AUTH
# smtp_user = airflow
# smtp_password = airflow
smtp_port = 25
smtp_mail_from = airflow@example.com
[sentry]
# Sentry (https://docs.sentry.io) integration
sentry_dsn =
[celery]
# This section only applies if you are using the CeleryExecutor in
# [core] section above
# The app name that will be used by celery
celery_app_name = airflow.executors.celery_executor
# The concurrency that will be used when starting workers with the
# "airflow worker" command. This defines the number of task instances that
# a worker will take, so size up your workers based on the resources on
# your worker box and the nature of your tasks
worker_concurrency = 40
# The maximum and minimum concurrency that will be used when starting workers with the
# "airflow worker" command (always keep minimum processes, but grow to maximum if necessary).
# Note the value should be "max_concurrency,min_concurrency"
# Pick these numbers based on resources on worker box and the nature of the task.
# If autoscale option is available, worker_concurrency will be ignored.
# http://docs.celeryproject.org/en/latest/reference/celery.bin.worker.html#cmdoption-celery-worker-autoscale
# worker_autoscale = 16,12
# When you start an airflow worker, airflow starts a tiny web server
# subprocess to serve the workers local log files to the airflow main
# web server, who then builds pages and sends them to users. This defines
# the port on which the logs are served. It needs to be unused, and open
# visible from the main web server to connect into the workers.
worker_log_server_port = 8793
# The Celery broker URL. Celery supports RabbitMQ, Redis and experimentally
# a sqlalchemy database. Refer to the Celery documentation for more
# information.
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#broker-settings
broker_url = sqla+mysql://airflow:airflow@localhost:3306/airflow
# The Celery result_backend. When a job finishes, it needs to update the
# metadata of the job. Therefore it will post a message on a message bus,
# or insert it into a database (depending of the backend)
# This status is used by the scheduler to update the state of the task
# The use of a database is highly recommended
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-result-backend-settings
result_backend = db+mysql://airflow:airflow@localhost:3306/airflow
# Celery Flower is a sweet UI for Celery. Airflow has a shortcut to start
# it `airflow flower`. This defines the IP that Celery Flower runs on
flower_host = 0.0.0.0
# The root URL for Flower
# Ex: flower_url_prefix = /flower
flower_url_prefix =
# This defines the port that Celery Flower runs on
flower_port = 5555
# Securing Flower with Basic Authentication
# Accepts user:password pairs separated by a comma
# Example: flower_basic_auth = user1:password1,user2:password2
flower_basic_auth =
# Default queue that tasks get assigned to and that worker listen on.
default_queue = default
# How many processes CeleryExecutor uses to sync task state.
# 0 means to use max(1, number of cores - 1) processes.
sync_parallelism = 0
# Import path for celery configuration options
celery_config_options = airflow.config_templates.default_celery.DEFAULT_CELERY_CONFIG
# In case of using SSL
ssl_active = False
ssl_key =
ssl_cert =
ssl_cacert =
# Celery Pool implementation.
# Choices include: prefork (default), eventlet, gevent or solo.
# See:
# https://docs.celeryproject.org/en/latest/userguide/workers.html#concurrency
# https://docs.celeryproject.org/en/latest/userguide/concurrency/eventlet.html
pool = prefork
[celery_broker_transport_options]
# This section is for specifying options which can be passed to the
# underlying celery broker transport. See:
# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_transport_options
# The visibility timeout defines the number of seconds to wait for the worker
# to acknowledge the task before the message is redelivered to another worker.
# Make sure to increase the visibility timeout to match the time of the longest
# ETA you're planning to use.
#
# visibility_timeout is only supported for Redis and SQS celery brokers.
# See:
# http://docs.celeryproject.org/en/master/userguide/configuration.html#std:setting-broker_transport_options
#
#visibility_timeout = 21600
[dask]
# This section only applies if you are using the DaskExecutor in
# [core] section above
# The IP address and port of the Dask cluster's scheduler.
cluster_address = 127.0.0.1:8786
# TLS/ SSL settings to access a secured Dask scheduler.
tls_ca =
tls_cert =
tls_key =
[scheduler]
# Task instances listen for external kill signal (when you clear tasks
# from the CLI or the UI), this defines the frequency at which they should
# listen (in seconds).
job_heartbeat_sec = 5
# The scheduler constantly tries to trigger new tasks (look at the
# scheduler section in the docs for more information). This defines
# how often the scheduler should run (in seconds).
scheduler_heartbeat_sec = 5
# after how much time should the scheduler terminate in seconds
# -1 indicates to run continuously (see also num_runs)
run_duration = -1
# The number of times to try to schedule each DAG file
# -1 indicates unlimited number
num_runs = -1
# The number of seconds to wait between consecutive DAG file processing
processor_poll_interval = 1
# after how much time (seconds) a new DAGs should be picked up from the filesystem
min_file_process_interval = 0
# How often (in seconds) to scan the DAGs directory for new files. Default to 5 minutes.
dag_dir_list_interval = 300
# How often should stats be printed to the logs
print_stats_interval = 30
# If the last scheduler heartbeat happened more than scheduler_health_check_threshold ago (in seconds),
# scheduler is considered unhealthy.
# This is used by the health check in the "/health" endpoint
scheduler_health_check_threshold = 30
child_process_log_directory = /airflow_home/logs/scheduler
# Local task jobs periodically heartbeat to the DB. If the job has
# not heartbeat in this many seconds, the scheduler will mark the
# associated task instance as failed and will re-schedule the task.
scheduler_zombie_task_threshold = 300
# Turn off scheduler catchup by setting this to False.
# Default behavior is unchanged and
# Command Line Backfills still work, but the scheduler
# will not do scheduler catchup if this is False,
# however it can be set on a per DAG basis in the
# DAG definition (catchup)
catchup_by_default = True
# This changes the batch size of queries in the scheduling main loop.
# If this is too high, SQL query performance may be impacted by one
# or more of the following:
# - reversion to full table scan
# - complexity of query predicate
# - excessive locking
#
# Additionally, you may hit the maximum allowable query length for your db.
#
# Set this to 0 for no limit (not advised)
max_tis_per_query = 512
# Statsd (https://github.com/etsy/statsd) integration settings
statsd_on = False
statsd_host = localhost
statsd_port = 8125
statsd_prefix = airflow
# If you want to avoid send all the available metrics to StatsD,
# you can configure an allow list of prefixes to send only the metrics that
# start with the elements of the list (e.g: scheduler,executor,dagrun)
statsd_allow_list =
# The scheduler can run multiple threads in parallel to schedule dags.
# This defines how many threads will run.
max_threads = 8
authenticate = False
# Turn off scheduler use of cron intervals by setting this to False.
# DAGs submitted manually in the web UI or with trigger_dag will still run.
use_job_schedule = True
[ldap]
# set this to ldaps://<your.ldap.server>:<port>
uri =
user_filter = objectClass=*
user_name_attr = uid
group_member_attr = memberOf
superuser_filter =
data_profiler_filter =
bind_user = cn=Manager,dc=example,dc=com
bind_password = insecure
basedn = dc=example,dc=com
cacert = /etc/ca/ldap_ca.crt
search_scope = LEVEL
# This setting allows the use of LDAP servers that either return a
# broken schema, or do not return a schema.
ignore_malformed_schema = False
[mesos]
# Mesos master address which MesosExecutor will connect to.
master = localhost:5050
# The framework name which Airflow scheduler will register itself as on mesos
framework_name = Airflow
# Number of cpu cores required for running one task instance using
# 'airflow run <dag_id> <task_id> <execution_date> --local -p <pickle_id>'
# command on a mesos slave
task_cpu = 1
# Memory in MB required for running one task instance using
# 'airflow run <dag_id> <task_id> <execution_date> --local -p <pickle_id>'
# command on a mesos slave
task_memory = 256
# Enable framework checkpointing for mesos
# See http://mesos.apache.org/documentation/latest/slave-recovery/
checkpoint = False
# Failover timeout in milliseconds.
# When checkpointing is enabled and this option is set, Mesos waits
# until the configured timeout for
# the MesosExecutor framework to re-register after a failover. Mesos
# shuts down running tasks if the
# MesosExecutor framework fails to re-register within this timeframe.
# failover_timeout = 604800
# Enable framework authentication for mesos
# See http://mesos.apache.org/documentation/latest/configuration/
authenticate = False
# Mesos credentials, if authentication is enabled
# default_principal = admin
# default_secret = admin
# Optional Docker Image to run on slave before running the command
# This image should be accessible from mesos slave i.e mesos slave
# should be able to pull this docker image before executing the command.
# docker_image_slave = puckel/docker-airflow
[kerberos]
ccache = /tmp/airflow_krb5_ccache
# gets augmented with fqdn
principal = airflow
reinit_frequency = 3600
kinit_path = kinit
keytab = airflow.keytab
[github_enterprise]
api_rev = v3
[admin]
# UI to hide sensitive variable fields when set to True
hide_sensitive_variable_fields = True
[elasticsearch]
# Elasticsearch host
host =
# Format of the log_id, which is used to query for a given tasks logs
log_id_template = {dag_id}-{task_id}-{execution_date}-{try_number}
# Used to mark the end of a log stream for a task
end_of_log_mark = end_of_log
# Qualified URL for an elasticsearch frontend (like Kibana) with a template argument for log_id
# Code will construct log_id using the log_id template from the argument above.
# NOTE: The code will prefix the https:// automatically, don't include that here.
frontend =
# Write the task logs to the stdout of the worker, rather than the default files
write_stdout = False
# Instead of the default log formatter, write the log lines as JSON
json_format = False
# Log fields to also attach to the json output, if enabled
json_fields = asctime, filename, lineno, levelname, message
[elasticsearch_configs]
use_ssl = False
verify_certs = True
[kubernetes]
# The repository, tag and imagePullPolicy of the Kubernetes Image for the Worker to Run
worker_container_repository =
worker_container_tag =
worker_container_image_pull_policy = IfNotPresent
# If True (default), worker pods will be deleted upon termination
delete_worker_pods = True
# Number of Kubernetes Worker Pod creation calls per scheduler loop
worker_pods_creation_batch_size = 1
# The Kubernetes namespace where airflow workers should be created. Defaults to `default`
namespace = default
# The name of the Kubernetes ConfigMap Containing the Airflow Configuration (this file)
airflow_configmap =
# For docker image already contains DAGs, this is set to `True`, and the worker will search for dags in dags_folder,
# otherwise use git sync or dags volume claim to mount DAGs
dags_in_image = False
# For either git sync or volume mounted DAGs, the worker will look in this subpath for DAGs
dags_volume_subpath =
# For DAGs mounted via a volume claim (mutually exclusive with git-sync and host path)
dags_volume_claim =
# For volume mounted logs, the worker will look in this subpath for logs
logs_volume_subpath =
# A shared volume claim for the logs
logs_volume_claim =
# For DAGs mounted via a hostPath volume (mutually exclusive with volume claim and git-sync)
# Useful in local environment, discouraged in production
dags_volume_host =
# A hostPath volume for the logs
# Useful in local environment, discouraged in production
logs_volume_host =
# A list of configMapsRefs to envFrom. If more than one configMap is
# specified, provide a comma separated list: configmap_a,configmap_b
env_from_configmap_ref =
# A list of secretRefs to envFrom. If more than one secret is
# specified, provide a comma separated list: secret_a,secret_b
env_from_secret_ref =
# Git credentials and repository for DAGs mounted via Git (mutually exclusive with volume claim)
git_repo =
git_branch =
git_subpath =
# Use git_user and git_password for user authentication or git_ssh_key_secret_name and git_ssh_key_secret_key
# for SSH authentication
git_user =
git_password =
git_sync_root = /git
git_sync_dest = repo
git_dags_folder_mount_point =
# To get Git-sync SSH authentication set up follow this format
#
# airflow-secrets.yaml:
# ---
# apiVersion: v1
# kind: Secret
# metadata:
# name: airflow-secrets
# data:
# # key needs to be gitSshKey
# gitSshKey: <base64_encoded_data>
# ---
# airflow-configmap.yaml:
# apiVersion: v1
# kind: ConfigMap
# metadata:
# name: airflow-configmap
# data:
# known_hosts: |
# github.com ssh-rsa <...>
# airflow.cfg: |
# ...
#
# git_ssh_key_secret_name = airflow-secrets
# git_ssh_known_hosts_configmap_name = airflow-configmap
git_ssh_key_secret_name =
git_ssh_known_hosts_configmap_name =
# To give the git_sync init container credentials via a secret, create a secret
# with two fields: GIT_SYNC_USERNAME and GIT_SYNC_PASSWORD (example below) and
# add `git_sync_credentials_secret = <secret_name>` to your airflow config under the kubernetes section
#
# Secret Example:
# apiVersion: v1
# kind: Secret
# metadata:
# name: git-credentials
# data:
# GIT_SYNC_USERNAME: <base64_encoded_git_username>
# GIT_SYNC_PASSWORD: <base64_encoded_git_password>
git_sync_credentials_secret =
# For cloning DAGs from git repositories into volumes: https://github.com/kubernetes/git-sync
git_sync_container_repository = k8s.gcr.io/git-sync
git_sync_container_tag = v3.1.1
git_sync_init_container_name = git-sync-clone
git_sync_run_as_user = 65533
# The name of the Kubernetes service account to be associated with airflow workers, if any.
# Service accounts are required for workers that require access to secrets or cluster resources.
# See the Kubernetes RBAC documentation for more:
# https://kubernetes.io/docs/admin/authorization/rbac/
worker_service_account_name =
# Any image pull secrets to be given to worker pods, If more than one secret is
# required, provide a comma separated list: secret_a,secret_b
image_pull_secrets =
# GCP Service Account Keys to be provided to tasks run on Kubernetes Executors
# Should be supplied in the format: key-name-1:key-path-1,key-name-2:key-path-2
gcp_service_account_keys =
# Use the service account kubernetes gives to pods to connect to kubernetes cluster.
# It's intended for clients that expect to be running inside a pod running on kubernetes.
# It will raise an exception if called from a process not running in a kubernetes environment.
in_cluster = True
# When running with in_cluster=False change the default cluster_context or config_file
# options to Kubernetes client. Leave blank these to use default behaviour like `kubectl` has.
# cluster_context =
# config_file =
# Affinity configuration as a single line formatted JSON object.
# See the affinity model for top-level key names (e.g. `nodeAffinity`, etc.):
# https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.12/#affinity-v1-core
affinity =
# A list of toleration objects as a single line formatted JSON array
# See:
# https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.12/#toleration-v1-core
tolerations =
# **kwargs parameters to pass while calling a kubernetes client core_v1_api methods from Kubernetes Executor
# provided as a single line formatted JSON dictionary string.
# List of supported params in **kwargs are similar for all core_v1_apis, hence a single config variable for all apis
# See:
# https://raw.githubusercontent.com/kubernetes-client/python/master/kubernetes/client/apis/core_v1_api.py
# Note that if no _request_timeout is specified, the kubernetes client will wait indefinitely for kubernetes
# api responses, which will cause the scheduler to hang. The timeout is specified as [connect timeout, read timeout]
kube_client_request_args = {"_request_timeout" : [60,60] }
# Worker pods security context options
# See:
# https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
# Specifies the uid to run the first process of the worker pods containers as
run_as_user =
# Specifies a gid to associate with all containers in the worker pods
# if using a git_ssh_key_secret_name use an fs_group
# that allows for the key to be read, e.g. 65533
fs_group =
[kubernetes_node_selectors]
# The Key-value pairs to be given to worker pods.
# The worker pods will be scheduled to the nodes of the specified key-value pairs.
# Should be supplied in the format: key = value
[kubernetes_annotations]
# The Key-value annotations pairs to be given to worker pods.
# Should be supplied in the format: key = value
[kubernetes_environment_variables]
# The scheduler sets the following environment variables into your workers. You may define as
# many environment variables as needed and the kubernetes launcher will set them in the launched workers.
# Environment variables in this section are defined as follows
# <environment_variable_key> = <environment_variable_value>
#
# For example if you wanted to set an environment variable with value `prod` and key
# `ENVIRONMENT` you would follow the following format:
# ENVIRONMENT = prod
#
# Additionally you may override worker airflow settings with the AIRFLOW__<SECTION>__<KEY>
# formatting as supported by airflow normally.
[kubernetes_secrets]
# The scheduler mounts the following secrets into your workers as they are launched by the
# scheduler. You may define as many secrets as needed and the kubernetes launcher will parse the
# defined secrets and mount them as secret environment variables in the launched workers.
# Secrets in this section are defined as follows
# <environment_variable_mount> = <kubernetes_secret_object>=<kubernetes_secret_key>
#
# For example if you wanted to mount a kubernetes secret key named `postgres_password` from the
# kubernetes secret object `airflow-secret` as the environment variable `POSTGRES_PASSWORD` into
# your workers you would follow the following format:
# POSTGRES_PASSWORD = airflow-secret=postgres_credentials
#
# Additionally you may override worker airflow settings with the AIRFLOW__<SECTION>__<KEY>
# formatting as supported by airflow normally.
[kubernetes_labels]
# The Key-value pairs to be given to worker pods.
# The worker pods will be given these static labels, as well as some additional dynamic labels
# to identify the task.
# Should be supplied in the format: key = value

View file

@ -0,0 +1,82 @@
[Main]
# The path where the planet will be downloaded and the maps are generated.
MAIN_OUT_PATH: /maps_build
# If the flag DEBUG is set a special small planet file will be downloaded.
DEBUG: 1
[Developer]
# The path where the generator_tool will be searched.
BUILD_PATH: /omim-build-release
# The path to the project directory omim.
OMIM_PATH: /omim
[Storage]
# Webdaw settings.
WD_HOST: webdav
WD_LOGIN: alice
WD_PASSWORD: secret1234
[Generator tool]
# The path to the omim/data.
USER_RESOURCE_PATH: ${Developer:OMIM_PATH}/data
# Do not change it. This is determined automatically.
# NODE_STORAGE: map
[Osm tools]
# The path to the osmctools sources.
OSM_TOOLS_SRC_PATH: ${Developer:OMIM_PATH}/tools/osmctools
# The path where osmctools will be searched or will be built.
OSM_TOOLS_PATH: /osmctools
[Stages]
# Run osmupdate tool for planet.
NEED_PLANET_UPDATE: 0
[Logging]
# The path where maps_generator log will be saved.
# LOG_FILE_PATH: generation.log
[External]
# The url to the planet file.
# PLANET_URL:
# The url to the file with md5 sum of the planet.
# PLANET_MD5_URL:
# The base url to WorldCoasts.geom and WorldCoasts.rawgeom (without file name).
# Files latest_coasts.geom and latest_coasts.rawgeom must be at this URL.
# For example, if PLANET_COASTS_URL = https://somesite.com/download/
# The https://somesite.com/download/latest_coasts.geom url will be used to download latest_coasts.geom and
# the https://somesite.com/download/latest_coasts.rawgeom url will be used to download latest_coasts.rawgeom.
# PLANET_COASTS_URL:
# The url to the subway file.
SUBWAY_URL: https://cdn.organicmaps.app/subway.json
# Urls for production maps generation.
# UGC_URL:
# HOTELS_URL:
# PROMO_CATALOG_CITIES:
# POPULARITY_URL:
# FOOD_URL:
# FOOD_TRANSLATIONS_URL:
# SRTM_PATH:
# ISOLINES_PATH:
# UK_POSTCODES_URL:
# US_POSTCODES_URL:
[Common]
# Auto detection.
THREADS_COUNT: 0
# Emails for mailing.
# EMAILS:
[Stats]
# Path to rules for calculating statistics by type
STATS_TYPES_CONFIG: ${Developer:OMIM_PATH}/tools/python/maps_generator/var/etc/stats_types_config.txt

View file

@ -0,0 +1,13 @@
#!/usr/bin/env bash
export PYTHONPATH=/omim/tools/python
export AIRFLOW_HOME=/airflow_home
# Initialize the database.
airflow initdb
# Start the web server, default port is 8880.
airflow webserver -p 8880 &
# Start the scheduler.
airflow scheduler

View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
BUILD_PATH="$(dirname "$0")"
OMIM_PATH="$(cd "${OMIM_PATH:-${BUILD_PATH}/../../../..}"; pwd)"
echo "Build airmaps service.."
mv "${OMIM_PATH}/.dockerignore" "${OMIM_PATH}/.dockerignore_" 2> /dev/null
cp "${BUILD_PATH}/.dockerignore" ${OMIM_PATH}
docker-compose build
rm "${OMIM_PATH}/.dockerignore"
mv "${OMIM_PATH}/.dockerignore_" "${OMIM_PATH}/.dockerignore" 2> /dev/null

View file

@ -0,0 +1,10 @@
#!/usr/bin/env bash
BUILD_PATH="$(dirname "$0")"
OMIM_PATH="$(cd "${OMIM_PATH:-${BUILD_PATH}/../../../..}"; pwd)"
echo "Cleaning.."
rm "${OMIM_PATH}/.dockerignore" 2> /dev/null
mv "${OMIM_PATH}/.dockerignore_" "${OMIM_PATH}/.dockerignore" 2> /dev/null
rm -r "${BUILD_PATH}/storage" 2> /dev/null

View file

@ -0,0 +1,10 @@
#!/usr/bin/env bash
BUILD_PATH="$(dirname "$0")"
echo "Creating storage.."
mkdir -p "${BUILD_PATH}/storage/tests/coasts"
mkdir -p "${BUILD_PATH}/storage/tests/maps/open_source"
mkdir -p "${BUILD_PATH}/storage/tests/planet_regular"
chown -R www-data:www-data "${BUILD_PATH}/storage/tests"

View file

@ -0,0 +1,41 @@
version: "3"
services:
webdav:
image: sashgorokhov/webdav
container_name: webdav
hostname: webdav
restart: always
environment:
USERNAME: alice
PASSWORD: secret1234
volumes:
- ./storage/tests:/media/tests
db:
image: postgres:12.2
container_name: db
hostname: db
restart: always
environment:
POSTGRES_DB: airflow
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
airmaps:
build:
context: ../../../..
dockerfile: ./tools/python/airmaps/sandbox/airmaps/Dockerfile
args:
- TZ=Europe/Moscow
container_name: airmaps
hostname: airmaps
restart: always
links:
- webdav
- db
ports:
- "80:8880"
command: ./tools/python/airmaps/sandbox/airmaps/run_airmaps_service.sh
volumes:
- ./airmaps/airflow.cfg:/airflow_home/airflow.cfg
- ./airmaps/airmaps.ini:/omim/tools/python/airmaps/var/etc/airmaps.ini

Some files were not shown because too many files have changed in this diff Show more