Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
49
libs/editor/CMakeLists.txt
Normal file
49
libs/editor/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
project(editor)
|
||||
|
||||
set(SRC
|
||||
changeset_wrapper.cpp
|
||||
changeset_wrapper.hpp
|
||||
config_loader.cpp
|
||||
config_loader.hpp
|
||||
editable_data_source.hpp
|
||||
editable_feature_source.cpp
|
||||
editable_feature_source.hpp
|
||||
editor_config.cpp
|
||||
editor_config.hpp
|
||||
editor_notes.cpp
|
||||
editor_notes.hpp
|
||||
editor_storage.cpp
|
||||
editor_storage.hpp
|
||||
edits_migration.cpp
|
||||
edits_migration.hpp
|
||||
feature_matcher.cpp
|
||||
feature_matcher.hpp
|
||||
new_feature_categories.cpp
|
||||
new_feature_categories.hpp
|
||||
opening_hours_ui.cpp
|
||||
opening_hours_ui.hpp
|
||||
osm_auth.cpp
|
||||
osm_auth.hpp
|
||||
osm_editor.cpp
|
||||
osm_editor.hpp
|
||||
server_api.cpp
|
||||
server_api.hpp
|
||||
ui2oh.cpp
|
||||
ui2oh.hpp
|
||||
xml_feature.cpp
|
||||
xml_feature.hpp
|
||||
yes_no_unknown.hpp
|
||||
keys_to_remove.hpp
|
||||
)
|
||||
|
||||
omim_add_library(${PROJECT_NAME} ${SRC})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
indexer
|
||||
opening_hours
|
||||
pugixml
|
||||
)
|
||||
|
||||
omim_add_test_subdirectory(editor_tests)
|
||||
omim_add_test_subdirectory(editor_tests_support)
|
||||
omim_add_test_subdirectory(osm_auth_tests)
|
||||
364
libs/editor/changeset_wrapper.cpp
Normal file
364
libs/editor/changeset_wrapper.cpp
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
#include "editor/changeset_wrapper.hpp"
|
||||
#include "editor/feature_matcher.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
#include "base/macros.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
m2::RectD GetBoundingRect(std::vector<m2::PointD> const & geometry)
|
||||
{
|
||||
m2::RectD rect;
|
||||
for (auto const & p : geometry)
|
||||
{
|
||||
auto const latLon = mercator::ToLatLon(p);
|
||||
rect.Add({latLon.m_lon, latLon.m_lat});
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
bool OsmFeatureHasTags(pugi::xml_node const & osmFt)
|
||||
{
|
||||
return osmFt.child("tag");
|
||||
}
|
||||
|
||||
std::string_view constexpr kVowels = "aeiouy";
|
||||
|
||||
std::string_view constexpr kMainTags[] = {"amenity", "shop", "tourism", "historic", "craft", "emergency",
|
||||
"barrier", "highway", "office", "leisure", "waterway", "natural",
|
||||
"place", "entrance", "building", "man_made", "healthcare", "attraction"};
|
||||
|
||||
std::string GetTypeForFeature(editor::XMLFeature const & node)
|
||||
{
|
||||
for (std::string_view const key : kMainTags)
|
||||
{
|
||||
if (node.HasTag(key))
|
||||
{
|
||||
// Non-const for RVO.
|
||||
std::string value = node.GetTagValue(key);
|
||||
if (value == "yes")
|
||||
return std::string{key};
|
||||
else if (key == "shop" || key == "office" || key == "building" || key == "entrance" || key == "attraction")
|
||||
return value.append(" ").append(key); // "convenience shop"
|
||||
else if (!value.empty() && value.back() == 's')
|
||||
// Remove 's' from the tail: "toilets" -> "toilet".
|
||||
return value.erase(value.size() - 1);
|
||||
else if (key == "healthcare" && value == "alternative")
|
||||
return "alternative medicine";
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.HasTag("disused:shop") || node.HasTag("disused:amenity"))
|
||||
return "vacant business";
|
||||
|
||||
if (node.HasTag("addr:housenumber") || node.HasTag("addr:street") || node.HasTag("addr:postcode"))
|
||||
return "address";
|
||||
|
||||
// Did not find any known tags.
|
||||
return node.HasAnyTags() ? "unknown object" : "empty object";
|
||||
}
|
||||
|
||||
std::vector<m2::PointD> NaiveSample(std::vector<m2::PointD> const & source, size_t count)
|
||||
{
|
||||
count = std::min(count, source.size());
|
||||
std::vector<m2::PointD> result;
|
||||
result.reserve(count);
|
||||
std::vector<size_t> indexes;
|
||||
indexes.reserve(count);
|
||||
|
||||
std::random_device r;
|
||||
std::minstd_rand engine(r());
|
||||
std::uniform_int_distribution<size_t> distrib(0, source.size());
|
||||
|
||||
while (count--)
|
||||
{
|
||||
size_t index;
|
||||
do
|
||||
{
|
||||
index = distrib(engine);
|
||||
}
|
||||
while (find(begin(indexes), end(indexes), index) != end(indexes));
|
||||
result.push_back(source[index]);
|
||||
indexes.push_back(index);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace pugi
|
||||
{
|
||||
std::string DebugPrint(xml_document const & doc)
|
||||
{
|
||||
std::ostringstream stream;
|
||||
doc.print(stream, " ");
|
||||
return stream.str();
|
||||
}
|
||||
} // namespace pugi
|
||||
|
||||
namespace osm
|
||||
{
|
||||
ChangesetWrapper::ChangesetWrapper(std::string const & keySecret, ServerApi06::KeyValueTags comments) noexcept
|
||||
: m_changesetComments(std::move(comments))
|
||||
, m_api(OsmOAuth::ServerAuth(keySecret))
|
||||
{}
|
||||
|
||||
ChangesetWrapper::~ChangesetWrapper()
|
||||
{
|
||||
if (m_changesetId)
|
||||
{
|
||||
try
|
||||
{
|
||||
AddChangesetTag("comment", GetDescription());
|
||||
m_api.UpdateChangeSet(m_changesetId, m_changesetComments);
|
||||
m_api.CloseChangeSet(m_changesetId);
|
||||
}
|
||||
catch (std::exception const & ex)
|
||||
{
|
||||
LOG(LWARNING, (ex.what()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChangesetWrapper::LoadXmlFromOSM(ms::LatLon const & ll, pugi::xml_document & doc, double radiusInMeters)
|
||||
{
|
||||
auto const response = m_api.GetXmlFeaturesAtLatLon(ll.m_lat, ll.m_lon, radiusInMeters);
|
||||
if (response.first != OsmOAuth::HTTP::OK)
|
||||
MYTHROW(HttpErrorException, ("HTTP error", response, "with GetXmlFeaturesAtLatLon", ll));
|
||||
|
||||
if (pugi::status_ok != doc.load_string(response.second.c_str()).status)
|
||||
MYTHROW(OsmXmlParseException,
|
||||
("Can't parse OSM server response for GetXmlFeaturesAtLatLon request", response.second));
|
||||
}
|
||||
|
||||
void ChangesetWrapper::LoadXmlFromOSM(ms::LatLon const & min, ms::LatLon const & max, pugi::xml_document & doc)
|
||||
{
|
||||
auto const response = m_api.GetXmlFeaturesInRect(min.m_lat, min.m_lon, max.m_lat, max.m_lon);
|
||||
if (response.first != OsmOAuth::HTTP::OK)
|
||||
MYTHROW(HttpErrorException, ("HTTP error", response, "with GetXmlFeaturesInRect", min, max));
|
||||
|
||||
if (pugi::status_ok != doc.load_string(response.second.c_str()).status)
|
||||
MYTHROW(OsmXmlParseException,
|
||||
("Can't parse OSM server response for GetXmlFeaturesInRect request", response.second));
|
||||
}
|
||||
|
||||
editor::XMLFeature ChangesetWrapper::GetMatchingNodeFeatureFromOSM(m2::PointD const & center)
|
||||
{
|
||||
// Match with OSM node.
|
||||
ms::LatLon const ll = mercator::ToLatLon(center);
|
||||
pugi::xml_document doc;
|
||||
// Throws!
|
||||
LoadXmlFromOSM(ll, doc);
|
||||
|
||||
pugi::xml_node const bestNode = matcher::GetBestOsmNode(doc, ll);
|
||||
if (bestNode.empty())
|
||||
{
|
||||
MYTHROW(OsmObjectWasDeletedException,
|
||||
("OSM does not have any nodes at the coordinates", ll, ", server has returned:", doc));
|
||||
}
|
||||
|
||||
if (!OsmFeatureHasTags(bestNode))
|
||||
{
|
||||
std::ostringstream sstr;
|
||||
bestNode.print(sstr);
|
||||
auto const strNode = sstr.str();
|
||||
LOG(LDEBUG, ("Node has no tags", strNode));
|
||||
MYTHROW(EmptyFeatureException, ("Node has no tags", strNode));
|
||||
}
|
||||
|
||||
return {bestNode};
|
||||
}
|
||||
|
||||
editor::XMLFeature ChangesetWrapper::GetMatchingAreaFeatureFromOSM(std::vector<m2::PointD> const & geometry)
|
||||
{
|
||||
auto constexpr kSamplePointsCount = 3;
|
||||
bool hasRelation = false;
|
||||
// Try several points in case of poor osm response.
|
||||
for (auto const & pt : NaiveSample(geometry, kSamplePointsCount))
|
||||
{
|
||||
ms::LatLon const ll = mercator::ToLatLon(pt);
|
||||
pugi::xml_document doc;
|
||||
// Throws!
|
||||
LoadXmlFromOSM(ll, doc);
|
||||
|
||||
if (doc.select_node("osm/relation"))
|
||||
{
|
||||
auto const rect = GetBoundingRect(geometry);
|
||||
LoadXmlFromOSM(ms::LatLon(rect.minY(), rect.minX()), ms::LatLon(rect.maxY(), rect.maxX()), doc);
|
||||
hasRelation = true;
|
||||
}
|
||||
|
||||
pugi::xml_node const bestWayOrRelation = matcher::GetBestOsmWayOrRelation(doc, geometry);
|
||||
if (!bestWayOrRelation)
|
||||
{
|
||||
if (hasRelation)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!OsmFeatureHasTags(bestWayOrRelation))
|
||||
{
|
||||
std::ostringstream sstr;
|
||||
bestWayOrRelation.print(sstr);
|
||||
LOG(LDEBUG, ("The matched object has no tags", sstr.str()));
|
||||
MYTHROW(EmptyFeatureException, ("The matched object has no tags"));
|
||||
}
|
||||
|
||||
return {bestWayOrRelation};
|
||||
}
|
||||
MYTHROW(OsmObjectWasDeletedException, ("OSM does not have any matching way for feature"));
|
||||
}
|
||||
|
||||
void ChangesetWrapper::Create(editor::XMLFeature node)
|
||||
{
|
||||
if (m_changesetId == kInvalidChangesetId)
|
||||
m_changesetId = m_api.CreateChangeSet(m_changesetComments);
|
||||
|
||||
// Changeset id should be updated for every OSM server commit.
|
||||
node.SetAttribute("changeset", strings::to_string(m_changesetId));
|
||||
// TODO(AlexZ): Think about storing/logging returned OSM ids.
|
||||
UNUSED_VALUE(m_api.CreateElement(node));
|
||||
m_created_types[GetTypeForFeature(node)]++;
|
||||
}
|
||||
|
||||
void ChangesetWrapper::Modify(editor::XMLFeature node)
|
||||
{
|
||||
if (m_changesetId == kInvalidChangesetId)
|
||||
m_changesetId = m_api.CreateChangeSet(m_changesetComments);
|
||||
|
||||
// Changeset id should be updated for every OSM server commit.
|
||||
node.SetAttribute("changeset", strings::to_string(m_changesetId));
|
||||
m_api.ModifyElement(node);
|
||||
m_modified_types[GetTypeForFeature(node)]++;
|
||||
}
|
||||
|
||||
void ChangesetWrapper::AddChangesetTag(std::string key, std::string value)
|
||||
{
|
||||
value = strings::EscapeForXML(value);
|
||||
|
||||
//OSM has a length limit of 255 characters
|
||||
if (value.length() > kMaximumOsmChars)
|
||||
{
|
||||
LOG(LWARNING, ("value is too long for OSM 255 char limit: ", value));
|
||||
value = value.substr(0, kMaximumOsmChars - 3).append("...");
|
||||
}
|
||||
|
||||
m_changesetComments.insert_or_assign(std::move(key), std::move(value));
|
||||
}
|
||||
|
||||
void ChangesetWrapper::AddToChangesetKeyList(std::string key, std::string value)
|
||||
{
|
||||
auto it = m_changesetComments.find(key);
|
||||
if (it == m_changesetComments.end())
|
||||
AddChangesetTag(std::move(key), std::move(value));
|
||||
else
|
||||
AddChangesetTag(std::move(key), it->second + "; " + value);
|
||||
}
|
||||
|
||||
void ChangesetWrapper::Delete(editor::XMLFeature node)
|
||||
{
|
||||
if (m_changesetId == kInvalidChangesetId)
|
||||
m_changesetId = m_api.CreateChangeSet(m_changesetComments);
|
||||
|
||||
// Changeset id should be updated for every OSM server commit.
|
||||
node.SetAttribute("changeset", strings::to_string(m_changesetId));
|
||||
m_api.DeleteElement(node);
|
||||
m_deleted_types[GetTypeForFeature(node)]++;
|
||||
}
|
||||
|
||||
std::string ChangesetWrapper::TypeCountToString(TypeCount const & typeCount)
|
||||
{
|
||||
if (typeCount.empty())
|
||||
return {};
|
||||
|
||||
// Convert map to vector and sort pairs by count, descending.
|
||||
std::vector<std::pair<std::string, size_t>> items{typeCount.begin(), typeCount.end()};
|
||||
|
||||
sort(items.begin(), items.end(), [](auto const & a, auto const & b) { return a.second > b.second; });
|
||||
|
||||
std::ostringstream ss;
|
||||
size_t const limit = std::min(size_t(3), items.size());
|
||||
for (size_t i = 0; i < limit; ++i)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
// Separator: "A and B" for two, "A, B, and C" for three or more.
|
||||
if (limit > 2)
|
||||
ss << ", ";
|
||||
else
|
||||
ss << " ";
|
||||
if (i == limit - 1)
|
||||
ss << "and ";
|
||||
}
|
||||
|
||||
auto & currentPair = items[i];
|
||||
// If we have more objects left, make the last one a list of these.
|
||||
if (i == limit - 1 && limit < items.size())
|
||||
{
|
||||
size_t count = 0;
|
||||
for (auto j = i; j < items.size(); ++j)
|
||||
count += items[j].second;
|
||||
currentPair = {"other object", count};
|
||||
}
|
||||
|
||||
// Format a count: "a shop" for single shop, "4 shops" for multiple.
|
||||
if (currentPair.second == 1)
|
||||
if (kVowels.find(currentPair.first.front()) != std::string::npos)
|
||||
ss << "an";
|
||||
else
|
||||
ss << "a";
|
||||
else
|
||||
ss << currentPair.second;
|
||||
ss << ' ' << currentPair.first;
|
||||
if (currentPair.second > 1)
|
||||
{
|
||||
if (currentPair.first.size() >= 2)
|
||||
{
|
||||
std::string const lastTwo = currentPair.first.substr(currentPair.first.size() - 2);
|
||||
// "bench" -> "benches", "marsh" -> "marshes", etc.
|
||||
if (lastTwo.back() == 'x' || lastTwo == "sh" || lastTwo == "ch" || lastTwo == "ss")
|
||||
{
|
||||
ss << 'e';
|
||||
}
|
||||
// "library" -> "libraries"
|
||||
else if (lastTwo.back() == 'y' && kVowels.find(lastTwo.front()) == std::string::npos)
|
||||
{
|
||||
ss.seekp(ss.tellp() - std::ostringstream::pos_type{1});
|
||||
ss << "ie";
|
||||
}
|
||||
}
|
||||
ss << 's';
|
||||
}
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string ChangesetWrapper::GetDescription() const
|
||||
{
|
||||
std::string result;
|
||||
if (!m_created_types.empty())
|
||||
result.append("Created ").append(TypeCountToString(m_created_types));
|
||||
if (!m_modified_types.empty())
|
||||
{
|
||||
if (!result.empty())
|
||||
result.append("; ");
|
||||
result.append("Updated ").append(TypeCountToString(m_modified_types));
|
||||
}
|
||||
if (!m_deleted_types.empty())
|
||||
{
|
||||
if (!result.empty())
|
||||
result.append("; ");
|
||||
result.append("Deleted ").append(TypeCountToString(m_deleted_types));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace osm
|
||||
77
libs/editor/changeset_wrapper.hpp
Normal file
77
libs/editor/changeset_wrapper.hpp
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#pragma once
|
||||
|
||||
#include "editor/server_api.hpp"
|
||||
#include "editor/xml_feature.hpp"
|
||||
|
||||
#include "geometry/point2d.hpp"
|
||||
#include "geometry/rect2d.hpp"
|
||||
|
||||
#include "base/exception.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class FeatureType;
|
||||
|
||||
namespace osm
|
||||
{
|
||||
class ChangesetWrapper
|
||||
{
|
||||
using TypeCount = std::map<std::string, size_t>;
|
||||
|
||||
public:
|
||||
DECLARE_EXCEPTION(ChangesetWrapperException, RootException);
|
||||
DECLARE_EXCEPTION(NetworkErrorException, ChangesetWrapperException);
|
||||
DECLARE_EXCEPTION(HttpErrorException, ChangesetWrapperException);
|
||||
DECLARE_EXCEPTION(OsmXmlParseException, ChangesetWrapperException);
|
||||
DECLARE_EXCEPTION(OsmObjectWasDeletedException, ChangesetWrapperException);
|
||||
DECLARE_EXCEPTION(CreateChangeSetFailedException, ChangesetWrapperException);
|
||||
DECLARE_EXCEPTION(ModifyNodeFailedException, ChangesetWrapperException);
|
||||
DECLARE_EXCEPTION(LinearFeaturesAreNotSupportedException, ChangesetWrapperException);
|
||||
DECLARE_EXCEPTION(EmptyFeatureException, ChangesetWrapperException);
|
||||
|
||||
ChangesetWrapper(std::string const & keySecret, ServerApi06::KeyValueTags comments) noexcept;
|
||||
~ChangesetWrapper();
|
||||
|
||||
/// Throws many exceptions from above list, plus including XMLNode's parsing ones.
|
||||
/// OsmObjectWasDeletedException means that node was deleted from OSM server by someone else.
|
||||
editor::XMLFeature GetMatchingNodeFeatureFromOSM(m2::PointD const & center);
|
||||
editor::XMLFeature GetMatchingAreaFeatureFromOSM(std::vector<m2::PointD> const & geomerty);
|
||||
|
||||
/// Throws exceptions from above list.
|
||||
void Create(editor::XMLFeature node);
|
||||
|
||||
/// Throws exceptions from above list.
|
||||
/// Node should have correct OSM "id" attribute set.
|
||||
void Modify(editor::XMLFeature node);
|
||||
|
||||
/// Throws exceptions from above list.
|
||||
void Delete(editor::XMLFeature node);
|
||||
|
||||
/// Add a tag to the changeset
|
||||
void AddChangesetTag(std::string key, std::string value);
|
||||
|
||||
/// Add item to ';' separated list for a changeset key
|
||||
void AddToChangesetKeyList(std::string key, std::string value);
|
||||
|
||||
private:
|
||||
/// Unfortunately, pugi can't return xml_documents from methods.
|
||||
/// Throws exceptions from above list.
|
||||
void LoadXmlFromOSM(ms::LatLon const & ll, pugi::xml_document & doc, double radiusInMeters = 1.0);
|
||||
void LoadXmlFromOSM(ms::LatLon const & min, ms::LatLon const & max, pugi::xml_document & doc);
|
||||
|
||||
ServerApi06::KeyValueTags m_changesetComments;
|
||||
ServerApi06 m_api;
|
||||
static constexpr uint64_t kInvalidChangesetId = 0;
|
||||
uint64_t m_changesetId = kInvalidChangesetId;
|
||||
static constexpr int kMaximumOsmChars = 255;
|
||||
|
||||
TypeCount m_modified_types;
|
||||
TypeCount m_created_types;
|
||||
TypeCount m_deleted_types;
|
||||
static std::string TypeCountToString(TypeCount const & typeCount);
|
||||
std::string GetDescription() const;
|
||||
};
|
||||
|
||||
} // namespace osm
|
||||
81
libs/editor/config_loader.cpp
Normal file
81
libs/editor/config_loader.cpp
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
#include "editor/config_loader.hpp"
|
||||
#include "editor/editor_config.hpp"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "coding/internal/file_data.hpp"
|
||||
#include "coding/reader.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace editor
|
||||
{
|
||||
using std::string;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr char kConfigFileName[] = "editor.config";
|
||||
|
||||
} // namespace
|
||||
|
||||
void Waiter::Interrupt()
|
||||
{
|
||||
{
|
||||
std::lock_guard lock(m_mutex);
|
||||
m_interrupted = true;
|
||||
}
|
||||
|
||||
m_event.notify_all();
|
||||
}
|
||||
|
||||
ConfigLoader::ConfigLoader(base::AtomicSharedPtr<EditorConfig> & config) : m_config(config)
|
||||
{
|
||||
pugi::xml_document doc;
|
||||
LoadFromLocal(doc);
|
||||
ResetConfig(doc);
|
||||
}
|
||||
|
||||
ConfigLoader::~ConfigLoader()
|
||||
{
|
||||
m_waiter.Interrupt();
|
||||
}
|
||||
|
||||
void ConfigLoader::ResetConfig(pugi::xml_document const & doc)
|
||||
{
|
||||
auto config = std::make_shared<EditorConfig>();
|
||||
config->SetConfig(doc);
|
||||
m_config.Set(config);
|
||||
}
|
||||
|
||||
// static
|
||||
void ConfigLoader::LoadFromLocal(pugi::xml_document & doc)
|
||||
{
|
||||
string content;
|
||||
std::unique_ptr<ModelReader> reader;
|
||||
|
||||
try
|
||||
{
|
||||
// Get config file from WritableDir first.
|
||||
reader = GetPlatform().GetReader(kConfigFileName, "wr");
|
||||
}
|
||||
catch (RootException const & ex)
|
||||
{
|
||||
LOG(LERROR, (ex.Msg()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (reader)
|
||||
reader->ReadAsString(content);
|
||||
|
||||
auto const result = doc.load_buffer(content.data(), content.size());
|
||||
if (!result)
|
||||
{
|
||||
LOG(LERROR, (kConfigFileName, "can not be loaded:", result.description(), "error offset:", result.offset));
|
||||
doc.reset();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace editor
|
||||
64
libs/editor/config_loader.hpp
Normal file
64
libs/editor/config_loader.hpp
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#pragma once
|
||||
|
||||
#include "base/atomic_shared_ptr.hpp"
|
||||
#include "base/exception.hpp"
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace pugi
|
||||
{
|
||||
class xml_document;
|
||||
}
|
||||
|
||||
namespace editor
|
||||
{
|
||||
class EditorConfig;
|
||||
|
||||
// Class for multithreaded interruptable waiting.
|
||||
class Waiter
|
||||
{
|
||||
public:
|
||||
template <typename Rep, typename Period>
|
||||
bool Wait(std::chrono::duration<Rep, Period> const & waitDuration)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_mutex);
|
||||
|
||||
if (m_interrupted)
|
||||
return false;
|
||||
|
||||
m_event.wait_for(lock, waitDuration, [this]() { return m_interrupted; });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Interrupt();
|
||||
|
||||
private:
|
||||
bool m_interrupted = false;
|
||||
std::mutex m_mutex;
|
||||
std::condition_variable m_event;
|
||||
};
|
||||
|
||||
// Class which loads config from local drive
|
||||
class ConfigLoader
|
||||
{
|
||||
public:
|
||||
explicit ConfigLoader(base::AtomicSharedPtr<EditorConfig> & config);
|
||||
~ConfigLoader();
|
||||
|
||||
// Static methods for production and testing.
|
||||
static void LoadFromLocal(pugi::xml_document & doc);
|
||||
|
||||
private:
|
||||
void ResetConfig(pugi::xml_document const & doc);
|
||||
|
||||
base::AtomicSharedPtr<EditorConfig> & m_config;
|
||||
|
||||
Waiter m_waiter;
|
||||
};
|
||||
} // namespace editor
|
||||
13
libs/editor/editable_data_source.hpp
Normal file
13
libs/editor/editable_data_source.hpp
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include "editor/editable_feature_source.hpp"
|
||||
|
||||
#include "indexer/data_source.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
class EditableDataSource : public DataSource
|
||||
{
|
||||
public:
|
||||
EditableDataSource() : DataSource(std::make_unique<EditableFeatureSourceFactory>()) {}
|
||||
};
|
||||
25
libs/editor/editable_feature_source.cpp
Normal file
25
libs/editor/editable_feature_source.cpp
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#include "editor/editable_feature_source.hpp"
|
||||
|
||||
#include "editor/osm_editor.hpp"
|
||||
|
||||
FeatureStatus EditableFeatureSource::GetFeatureStatus(uint32_t index) const
|
||||
{
|
||||
osm::Editor & editor = osm::Editor::Instance();
|
||||
return editor.GetFeatureStatus(m_handle.GetId(), index);
|
||||
}
|
||||
|
||||
std::unique_ptr<FeatureType> EditableFeatureSource::GetModifiedFeature(uint32_t index) const
|
||||
{
|
||||
osm::Editor & editor = osm::Editor::Instance();
|
||||
auto const emo = editor.GetEditedFeature(FeatureID(m_handle.GetId(), index));
|
||||
if (emo)
|
||||
return FeatureType::CreateFromMapObject(*emo);
|
||||
return {};
|
||||
}
|
||||
|
||||
void EditableFeatureSource::ForEachAdditionalFeature(m2::RectD const & rect, int scale,
|
||||
std::function<void(uint32_t)> const & fn) const
|
||||
{
|
||||
osm::Editor & editor = osm::Editor::Instance();
|
||||
editor.ForEachCreatedFeature(m_handle.GetId(), fn, rect, scale);
|
||||
}
|
||||
33
libs/editor/editable_feature_source.hpp
Normal file
33
libs/editor/editable_feature_source.hpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include "indexer/feature.hpp"
|
||||
#include "indexer/feature_source.hpp"
|
||||
#include "indexer/mwm_set.hpp"
|
||||
|
||||
#include "geometry/rect2d.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
class EditableFeatureSource final : public FeatureSource
|
||||
{
|
||||
public:
|
||||
explicit EditableFeatureSource(MwmSet::MwmHandle const & handle) : FeatureSource(handle) {}
|
||||
|
||||
// FeatureSource overrides:
|
||||
FeatureStatus GetFeatureStatus(uint32_t index) const override;
|
||||
std::unique_ptr<FeatureType> GetModifiedFeature(uint32_t index) const override;
|
||||
void ForEachAdditionalFeature(m2::RectD const & rect, int scale,
|
||||
std::function<void(uint32_t)> const & fn) const override;
|
||||
};
|
||||
|
||||
class EditableFeatureSourceFactory : public FeatureSourceFactory
|
||||
{
|
||||
public:
|
||||
// FeatureSourceFactory overrides:
|
||||
std::unique_ptr<FeatureSource> operator()(MwmSet::MwmHandle const & handle) const override
|
||||
{
|
||||
return std::make_unique<EditableFeatureSource>(handle);
|
||||
}
|
||||
};
|
||||
188
libs/editor/editor_config.cpp
Normal file
188
libs/editor/editor_config.cpp
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
#include "editor/editor_config.hpp"
|
||||
|
||||
#include "base/stl_helpers.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace editor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
using EType = feature::Metadata::EType;
|
||||
|
||||
// TODO(mgsergio): It would be nice to have this map generated from editor.config.
|
||||
static std::unordered_map<std::string, EType> const kNamesToFMD = {
|
||||
{"opening_hours", EType::FMD_OPEN_HOURS},
|
||||
{"phone", EType::FMD_PHONE_NUMBER},
|
||||
{"fax", EType::FMD_FAX_NUMBER},
|
||||
{"stars", EType::FMD_STARS},
|
||||
{"operator", EType::FMD_OPERATOR},
|
||||
{"website", EType::FMD_WEBSITE},
|
||||
{"contact_facebook", EType::FMD_CONTACT_FACEBOOK},
|
||||
{"contact_instagram", EType::FMD_CONTACT_INSTAGRAM},
|
||||
{"contact_fediverse", EType::FMD_CONTACT_FEDIVERSE},
|
||||
{"contact_bluesky", EType::FMD_CONTACT_BLUESKY},
|
||||
{"contact_twitter", EType::FMD_CONTACT_TWITTER},
|
||||
{"contact_vk", EType::FMD_CONTACT_VK},
|
||||
{"contact_line", EType::FMD_CONTACT_LINE},
|
||||
{"internet", EType::FMD_INTERNET},
|
||||
{"ele", EType::FMD_ELE},
|
||||
// {"", EType::FMD_TURN_LANES},
|
||||
// {"", EType::FMD_TURN_LANES_FORWARD},
|
||||
// {"", EType::FMD_TURN_LANES_BACKWARD},
|
||||
{"email", EType::FMD_EMAIL},
|
||||
{"postcode", EType::FMD_POSTCODE},
|
||||
{"wikipedia", EType::FMD_WIKIPEDIA},
|
||||
// {"", EType::FMD_MAXSPEED},
|
||||
{"flats", EType::FMD_FLATS},
|
||||
{"height", EType::FMD_HEIGHT},
|
||||
// {"", EType::FMD_MIN_HEIGHT},
|
||||
{"denomination", EType::FMD_DENOMINATION},
|
||||
{"building:levels", EType::FMD_BUILDING_LEVELS},
|
||||
{"level", EType::FMD_LEVEL},
|
||||
{"drive_through", EType::FMD_DRIVE_THROUGH},
|
||||
{"website_menu", EType::FMD_WEBSITE_MENU},
|
||||
{"self_service", EType::FMD_SELF_SERVICE},
|
||||
{"outdoor_seating", EType::FMD_OUTDOOR_SEATING},
|
||||
// TODO(skadge): this won't work, obv
|
||||
{"socket_type1_count", EType::FMD_CHARGE_SOCKETS},
|
||||
{"socket_type1_output", EType::FMD_CHARGE_SOCKETS},
|
||||
{"socket_type2_count", EType::FMD_CHARGE_SOCKETS},
|
||||
{"socket_type2_output", EType::FMD_CHARGE_SOCKETS},
|
||||
/// @todo Add description?
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, int> const kPriorityWeights = {{"high", 0}, {"", 1}, {"low", 2}};
|
||||
|
||||
bool TypeDescriptionFromXml(pugi::xml_node const & root, pugi::xml_node const & node,
|
||||
editor::TypeAggregatedDescription & outDesc)
|
||||
{
|
||||
if (!node || strcmp(node.attribute("editable").value(), "no") == 0)
|
||||
return false;
|
||||
|
||||
auto const handleField = [&outDesc](std::string const & fieldName)
|
||||
{
|
||||
if (fieldName == "name")
|
||||
{
|
||||
outDesc.m_name = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (fieldName == "street" || fieldName == "housenumber" || fieldName == "housename" || fieldName == "postcode")
|
||||
{
|
||||
outDesc.m_address = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (fieldName == "cuisine")
|
||||
{
|
||||
outDesc.m_cuisine = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO(mgsergio): Add support for non-metadata fields like atm, wheelchair, toilet etc.
|
||||
auto const it = kNamesToFMD.find(fieldName);
|
||||
|
||||
ASSERT(it != end(kNamesToFMD), ("Wrong field:", fieldName));
|
||||
outDesc.m_editableFields.push_back(it->second);
|
||||
};
|
||||
|
||||
for (auto const & xNode : node.select_nodes("include[@group]"))
|
||||
{
|
||||
auto const node = xNode.node();
|
||||
std::string const groupName = node.attribute("group").value();
|
||||
|
||||
std::string const xpath = "/comaps/editor/fields/field_group[@name='" + groupName + "']";
|
||||
auto const group = root.select_node(xpath.data()).node();
|
||||
ASSERT(group, ("No such group", groupName));
|
||||
|
||||
for (auto const & fieldRefXName : group.select_nodes("field_ref/@name"))
|
||||
{
|
||||
auto const fieldName = fieldRefXName.attribute().value();
|
||||
handleField(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const & xNode : node.select_nodes("include[@field]"))
|
||||
{
|
||||
auto const node = xNode.node();
|
||||
std::string const fieldName = node.attribute("field").value();
|
||||
handleField(fieldName);
|
||||
}
|
||||
|
||||
// Ordered by Metadata::EType value, which is also satisfy fields importance.
|
||||
base::SortUnique(outDesc.m_editableFields);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The priority is defined by elems order, except elements with priority="high".
|
||||
std::vector<pugi::xml_node> GetPrioritizedTypes(pugi::xml_node const & node)
|
||||
{
|
||||
std::vector<pugi::xml_node> result;
|
||||
for (auto const & xNode : node.select_nodes("/comaps/editor/types/type[@id]"))
|
||||
result.push_back(xNode.node());
|
||||
stable_sort(begin(result), end(result), [](pugi::xml_node const & lhs, pugi::xml_node const & rhs)
|
||||
{
|
||||
auto const lhsWeight = kPriorityWeights.find(lhs.attribute("priority").value());
|
||||
auto const rhsWeight = kPriorityWeights.find(rhs.attribute("priority").value());
|
||||
|
||||
CHECK(lhsWeight != kPriorityWeights.end(), (""));
|
||||
CHECK(rhsWeight != kPriorityWeights.end(), (""));
|
||||
|
||||
return lhsWeight->second < rhsWeight->second;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool EditorConfig::GetTypeDescription(std::vector<std::string> classificatorTypes,
|
||||
TypeAggregatedDescription & outDesc) const
|
||||
{
|
||||
bool isBuilding = false;
|
||||
std::vector<std::string> addTypes;
|
||||
for (auto it = classificatorTypes.begin(); it != classificatorTypes.end(); ++it)
|
||||
{
|
||||
if (*it == "building")
|
||||
{
|
||||
outDesc.m_address = isBuilding = true;
|
||||
outDesc.m_editableFields.push_back(EType::FMD_BUILDING_LEVELS);
|
||||
outDesc.m_editableFields.push_back(EType::FMD_POSTCODE);
|
||||
classificatorTypes.erase(it);
|
||||
break;
|
||||
}
|
||||
|
||||
// Adding partial types for 2..N-1 parts of a N-part type.
|
||||
auto hyphenPos = it->find('-');
|
||||
while ((hyphenPos = it->find('-', hyphenPos + 1)) != std::string::npos)
|
||||
addTypes.push_back(it->substr(0, hyphenPos));
|
||||
}
|
||||
|
||||
classificatorTypes.insert(classificatorTypes.end(), addTypes.begin(), addTypes.end());
|
||||
|
||||
auto const typeNodes = GetPrioritizedTypes(m_document);
|
||||
auto const it = base::FindIf(typeNodes, [&classificatorTypes](pugi::xml_node const & node)
|
||||
{ return base::IsExist(classificatorTypes, node.attribute("id").value()); });
|
||||
if (it == end(typeNodes))
|
||||
return isBuilding;
|
||||
|
||||
return TypeDescriptionFromXml(m_document, *it, outDesc);
|
||||
}
|
||||
|
||||
std::vector<std::string> EditorConfig::GetTypesThatCanBeAdded() const
|
||||
{
|
||||
auto const xpathResult = m_document.select_nodes("/comaps/editor/types/type[not(@can_add='no' or @editable='no')]");
|
||||
|
||||
std::vector<std::string> result;
|
||||
for (auto const & xNode : xpathResult)
|
||||
result.emplace_back(xNode.node().attribute("id").value());
|
||||
return result;
|
||||
}
|
||||
|
||||
void EditorConfig::SetConfig(pugi::xml_document const & doc)
|
||||
{
|
||||
m_document.reset(doc);
|
||||
}
|
||||
} // namespace editor
|
||||
55
libs/editor/editor_config.hpp
Normal file
55
libs/editor/editor_config.hpp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#pragma once
|
||||
|
||||
#include "indexer/feature_meta.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
class Reader;
|
||||
|
||||
namespace editor
|
||||
{
|
||||
struct TypeAggregatedDescription
|
||||
{
|
||||
using EType = feature::Metadata::EType;
|
||||
using FeatureFields = std::vector<EType>;
|
||||
|
||||
bool IsEmpty() const
|
||||
{
|
||||
return IsNameEditable() || IsAddressEditable() || IsCuisineEditable() || !m_editableFields.empty();
|
||||
}
|
||||
|
||||
FeatureFields const & GetEditableFields() const { return m_editableFields; }
|
||||
|
||||
bool IsNameEditable() const { return m_name; }
|
||||
bool IsAddressEditable() const { return m_address; }
|
||||
bool IsCuisineEditable() const { return m_cuisine; }
|
||||
|
||||
FeatureFields m_editableFields;
|
||||
|
||||
bool m_name = false;
|
||||
bool m_address = false;
|
||||
bool m_cuisine = false;
|
||||
};
|
||||
|
||||
class EditorConfig
|
||||
{
|
||||
public:
|
||||
EditorConfig() = default;
|
||||
|
||||
// TODO(mgsergio): Reduce overhead by matching uint32_t types instead of strings.
|
||||
bool GetTypeDescription(std::vector<std::string> classificatorTypes, TypeAggregatedDescription & outDesc) const;
|
||||
std::vector<std::string> GetTypesThatCanBeAdded() const;
|
||||
|
||||
void SetConfig(pugi::xml_document const & doc);
|
||||
|
||||
// TODO(mgsergio): Implement this getter to avoid hard-code in XMLFeature::ApplyPatch.
|
||||
// It should return [[phone, contact:phone, contact:mobile], [website, contact:website, url], ...].
|
||||
// vector<vector<string>> GetAlternativeFields() const;
|
||||
|
||||
private:
|
||||
pugi::xml_document m_document;
|
||||
};
|
||||
} // namespace editor
|
||||
210
libs/editor/editor_notes.cpp
Normal file
210
libs/editor/editor_notes.cpp
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
#include "editor/editor_notes.hpp"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "coding/internal/file_data.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/logging.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
#include "base/timer.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace
|
||||
{
|
||||
bool LoadFromXml(pugi::xml_document const & xml, std::list<editor::Note> & notes, uint32_t & uploadedNotesCount)
|
||||
{
|
||||
uint64_t notesCount;
|
||||
auto const root = xml.child("notes");
|
||||
if (!strings::to_uint64(root.attribute("uploadedNotesCount").value(), notesCount))
|
||||
{
|
||||
LOG(LERROR, ("Can't read uploadedNotesCount from file."));
|
||||
uploadedNotesCount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
uploadedNotesCount = static_cast<uint32_t>(notesCount);
|
||||
}
|
||||
|
||||
for (auto const & xNode : root.select_nodes("note"))
|
||||
{
|
||||
ms::LatLon latLon;
|
||||
|
||||
auto const node = xNode.node();
|
||||
auto const lat = node.attribute("lat");
|
||||
if (!lat || !strings::to_double(lat.value(), latLon.m_lat))
|
||||
continue;
|
||||
|
||||
auto const lon = node.attribute("lon");
|
||||
if (!lon || !strings::to_double(lon.value(), latLon.m_lon))
|
||||
continue;
|
||||
|
||||
auto const text = node.attribute("text");
|
||||
if (!text)
|
||||
continue;
|
||||
|
||||
notes.emplace_back(latLon, text.value());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SaveToXml(std::list<editor::Note> const & notes, pugi::xml_document & xml, uint32_t const uploadedNotesCount)
|
||||
{
|
||||
auto constexpr kDigitsAfterComma = 7;
|
||||
auto root = xml.append_child("notes");
|
||||
root.append_attribute("uploadedNotesCount") = uploadedNotesCount;
|
||||
for (auto const & note : notes)
|
||||
{
|
||||
auto node = root.append_child("note");
|
||||
|
||||
node.append_attribute("lat") = strings::to_string_dac(note.m_point.m_lat, kDigitsAfterComma).data();
|
||||
node.append_attribute("lon") = strings::to_string_dac(note.m_point.m_lon, kDigitsAfterComma).data();
|
||||
node.append_attribute("text") = note.m_note.data();
|
||||
}
|
||||
}
|
||||
|
||||
/// Not thread-safe, use only for initialization.
|
||||
bool Load(std::string const & fileName, std::list<editor::Note> & notes, uint32_t & uploadedNotesCount)
|
||||
{
|
||||
std::string content;
|
||||
try
|
||||
{
|
||||
auto const reader = GetPlatform().GetReader(fileName);
|
||||
reader->ReadAsString(content);
|
||||
}
|
||||
catch (FileAbsentException const &)
|
||||
{
|
||||
// It's normal if no notes file is present.
|
||||
return true;
|
||||
}
|
||||
catch (Reader::Exception const & e)
|
||||
{
|
||||
LOG(LERROR, ("Can't process file.", fileName, e.Msg()));
|
||||
return false;
|
||||
}
|
||||
|
||||
pugi::xml_document xml;
|
||||
if (!xml.load_buffer(content.data(), content.size()))
|
||||
{
|
||||
LOG(LERROR, ("Can't load notes, XML is ill-formed.", content));
|
||||
return false;
|
||||
}
|
||||
|
||||
notes.clear();
|
||||
if (!LoadFromXml(xml, notes, uploadedNotesCount))
|
||||
{
|
||||
LOG(LERROR, ("Can't load notes, file is ill-formed.", content));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Not thread-safe, use synchronization.
|
||||
bool Save(std::string const & fileName, std::list<editor::Note> const & notes, uint32_t const uploadedNotesCount)
|
||||
{
|
||||
pugi::xml_document xml;
|
||||
SaveToXml(notes, xml, uploadedNotesCount);
|
||||
return base::WriteToTempAndRenameToFile(
|
||||
fileName, [&xml](std::string const & fileName) { return xml.save_file(fileName.data(), " "); });
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace editor
|
||||
{
|
||||
std::shared_ptr<Notes> Notes::MakeNotes(std::string const & fileName, bool const fullPath)
|
||||
{
|
||||
return std::shared_ptr<Notes>(new Notes(fullPath ? fileName : GetPlatform().WritablePathForFile(fileName)));
|
||||
}
|
||||
|
||||
Notes::Notes(std::string const & fileName) : m_fileName(fileName)
|
||||
{
|
||||
Load(m_fileName, m_notes, m_uploadedNotesCount);
|
||||
}
|
||||
|
||||
void Notes::CreateNote(ms::LatLon const & latLon, std::string const & text)
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
LOG(LWARNING, ("Attempt to create empty note"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mercator::ValidLat(latLon.m_lat) || !mercator::ValidLon(latLon.m_lon))
|
||||
{
|
||||
LOG(LWARNING, ("A note attached to a wrong latLon", latLon));
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> g(m_dataAccessMutex);
|
||||
auto const it = std::find_if(m_notes.begin(), m_notes.end(), [&latLon, &text](Note const & note)
|
||||
{ return latLon.EqualDxDy(note.m_point, kTolerance) && text == note.m_note; });
|
||||
// No need to add the same note. It works in case when saved note are not uploaded yet.
|
||||
if (it != m_notes.end())
|
||||
return;
|
||||
|
||||
m_notes.emplace_back(latLon, text);
|
||||
Save(m_fileName, m_notes, m_uploadedNotesCount);
|
||||
}
|
||||
|
||||
void Notes::Upload(osm::OsmOAuth const & auth)
|
||||
{
|
||||
std::unique_lock<std::mutex> uploadingNotesLock(m_uploadingNotesMutex, std::defer_lock);
|
||||
if (!uploadingNotesLock.try_lock()) {
|
||||
// Do not run more than one uploading task at a time.
|
||||
LOG(LDEBUG, ("OSM notes upload is already running"));
|
||||
return;
|
||||
}
|
||||
std::unique_lock<std::mutex> dataAccessLock(m_dataAccessMutex);
|
||||
|
||||
// Size of m_notes is decreased only in this method.
|
||||
size_t size = m_notes.size();
|
||||
osm::ServerApi06 api(auth);
|
||||
|
||||
while (size > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
dataAccessLock.unlock();
|
||||
auto const id = api.CreateNote(m_notes.front().m_point, m_notes.front().m_note);
|
||||
dataAccessLock.lock();
|
||||
LOG(LINFO, ("A note uploaded with id", id));
|
||||
}
|
||||
catch (osm::ServerApi06::ServerApi06Exception const & e)
|
||||
{
|
||||
LOG(LERROR, ("Can't upload note.", e.Msg()));
|
||||
// Don't attempt upload for other notes as they will likely suffer from the same error.
|
||||
return;
|
||||
}
|
||||
|
||||
m_notes.pop_front();
|
||||
--size;
|
||||
++m_uploadedNotesCount;
|
||||
Save(m_fileName, m_notes, m_uploadedNotesCount);
|
||||
}
|
||||
}
|
||||
|
||||
std::list<Note> Notes::GetNotes() const
|
||||
{
|
||||
std::lock_guard<std::mutex> g(m_dataAccessMutex);
|
||||
return m_notes;
|
||||
}
|
||||
|
||||
size_t Notes::NotUploadedNotesCount() const
|
||||
{
|
||||
std::lock_guard<std::mutex> g(m_dataAccessMutex);
|
||||
return m_notes.size();
|
||||
}
|
||||
|
||||
size_t Notes::UploadedNotesCount() const
|
||||
{
|
||||
std::lock_guard<std::mutex> g(m_dataAccessMutex);
|
||||
return m_uploadedNotesCount;
|
||||
}
|
||||
} // namespace editor
|
||||
60
libs/editor/editor_notes.hpp
Normal file
60
libs/editor/editor_notes.hpp
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#pragma once
|
||||
|
||||
#include "editor/server_api.hpp"
|
||||
|
||||
#include "geometry/latlon.hpp"
|
||||
|
||||
#include "base/macros.hpp"
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace editor
|
||||
{
|
||||
struct Note
|
||||
{
|
||||
Note(ms::LatLon const & point, std::string const & text) : m_point(point), m_note(text) {}
|
||||
ms::LatLon m_point;
|
||||
std::string m_note;
|
||||
};
|
||||
|
||||
inline bool operator==(Note const & lhs, Note const & rhs)
|
||||
{
|
||||
return lhs.m_point == rhs.m_point && lhs.m_note == rhs.m_note;
|
||||
}
|
||||
|
||||
class Notes : public std::enable_shared_from_this<Notes>
|
||||
{
|
||||
public:
|
||||
static float constexpr kTolerance = 1e-7;
|
||||
static std::shared_ptr<Notes> MakeNotes(std::string const & fileName = "notes.xml", bool const fullPath = false);
|
||||
|
||||
void CreateNote(ms::LatLon const & latLon, std::string const & text);
|
||||
|
||||
/// Uploads notes to the server in a separate thread.
|
||||
/// Called on main thread from system event.
|
||||
void Upload(osm::OsmOAuth const & auth);
|
||||
|
||||
std::list<Note> GetNotes() const;
|
||||
|
||||
size_t NotUploadedNotesCount() const;
|
||||
size_t UploadedNotesCount() const;
|
||||
|
||||
private:
|
||||
explicit Notes(std::string const & fileName);
|
||||
|
||||
std::string const m_fileName;
|
||||
mutable std::mutex m_dataAccessMutex;
|
||||
mutable std::mutex m_uploadingNotesMutex;
|
||||
|
||||
// m_notes keeps the notes that have not been uploaded yet.
|
||||
// Once a note has been uploaded, it is removed from m_notes.
|
||||
std::list<Note> m_notes;
|
||||
|
||||
uint32_t m_uploadedNotesCount = 0;
|
||||
|
||||
DISALLOW_COPY_AND_MOVE(Notes);
|
||||
};
|
||||
} // namespace editor
|
||||
78
libs/editor/editor_storage.cpp
Normal file
78
libs/editor/editor_storage.cpp
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#include "editor/editor_storage.hpp"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "coding/internal/file_data.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace pugi;
|
||||
|
||||
namespace
|
||||
{
|
||||
char const * kEditorXMLFileName = "edits.xml";
|
||||
|
||||
std::string GetEditorFilePath()
|
||||
{
|
||||
return GetPlatform().WritablePathForFile(kEditorXMLFileName);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace editor
|
||||
{
|
||||
// StorageLocal ------------------------------------------------------------------------------------
|
||||
bool LocalStorage::Save(xml_document const & doc)
|
||||
{
|
||||
auto const editorFilePath = GetEditorFilePath();
|
||||
|
||||
std::lock_guard<std::mutex> guard(m_mutex);
|
||||
|
||||
return base::WriteToTempAndRenameToFile(editorFilePath, [&doc](std::string const & fileName)
|
||||
{ return doc.save_file(fileName.data(), " " /* indent */); });
|
||||
}
|
||||
|
||||
bool LocalStorage::Load(xml_document & doc)
|
||||
{
|
||||
auto const editorFilePath = GetEditorFilePath();
|
||||
|
||||
std::lock_guard<std::mutex> guard(m_mutex);
|
||||
|
||||
auto const result = doc.load_file(editorFilePath.c_str());
|
||||
// Note: status_file_not_found is ok if a user has never made any edits.
|
||||
if (result != status_ok && result != status_file_not_found)
|
||||
{
|
||||
LOG(LERROR, ("Can't load map edits from disk:", editorFilePath));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocalStorage::Reset()
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(m_mutex);
|
||||
|
||||
return base::DeleteFileX(GetEditorFilePath());
|
||||
}
|
||||
|
||||
// StorageMemory -----------------------------------------------------------------------------------
|
||||
bool InMemoryStorage::Save(xml_document const & doc)
|
||||
{
|
||||
m_doc.reset(doc);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InMemoryStorage::Load(xml_document & doc)
|
||||
{
|
||||
doc.reset(m_doc);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InMemoryStorage::Reset()
|
||||
{
|
||||
m_doc.reset();
|
||||
return true;
|
||||
}
|
||||
} // namespace editor
|
||||
47
libs/editor/editor_storage.hpp
Normal file
47
libs/editor/editor_storage.hpp
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace editor
|
||||
{
|
||||
// Editor storage interface.
|
||||
class StorageBase
|
||||
{
|
||||
public:
|
||||
virtual ~StorageBase() = default;
|
||||
|
||||
virtual bool Save(pugi::xml_document const & doc) = 0;
|
||||
virtual bool Load(pugi::xml_document & doc) = 0;
|
||||
virtual bool Reset() = 0;
|
||||
};
|
||||
|
||||
// Class which saves/loads edits to/from local file.
|
||||
// Note: this class IS thread-safe.
|
||||
class LocalStorage : public StorageBase
|
||||
{
|
||||
public:
|
||||
// StorageBase overrides:
|
||||
bool Save(pugi::xml_document const & doc) override;
|
||||
bool Load(pugi::xml_document & doc) override;
|
||||
bool Reset() override;
|
||||
|
||||
private:
|
||||
std::mutex m_mutex;
|
||||
};
|
||||
|
||||
// Class which saves/loads edits to/from xml_document class instance.
|
||||
// Note: this class is NOT thread-safe.
|
||||
class InMemoryStorage : public StorageBase
|
||||
{
|
||||
public:
|
||||
// StorageBase overrides:
|
||||
bool Save(pugi::xml_document const & doc) override;
|
||||
bool Load(pugi::xml_document & doc) override;
|
||||
bool Reset() override;
|
||||
|
||||
private:
|
||||
pugi::xml_document m_doc;
|
||||
};
|
||||
} // namespace editor
|
||||
25
libs/editor/editor_tests/CMakeLists.txt
Normal file
25
libs/editor/editor_tests/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
project(editor_tests)
|
||||
|
||||
set(SRC
|
||||
config_loader_test.cpp
|
||||
editor_config_test.cpp
|
||||
editor_notes_test.cpp
|
||||
feature_matcher_test.cpp
|
||||
match_by_geometry_test.cpp
|
||||
new_feature_categories_test.cpp
|
||||
opening_hours_ui_test.cpp
|
||||
osm_editor_test.cpp
|
||||
osm_editor_test.hpp
|
||||
ui2oh_test.cpp
|
||||
xml_feature_test.cpp
|
||||
)
|
||||
|
||||
omim_add_test(${PROJECT_NAME} ${SRC})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
editor_tests_support
|
||||
platform_tests_support
|
||||
generator_tests_support
|
||||
search
|
||||
indexer
|
||||
)
|
||||
55
libs/editor/editor_tests/config_loader_test.cpp
Normal file
55
libs/editor/editor_tests/config_loader_test.cpp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "editor/config_loader.hpp"
|
||||
#include "editor/editor_config.hpp"
|
||||
|
||||
#include "platform/platform_tests_support/scoped_file.hpp"
|
||||
|
||||
#include "base/atomic_shared_ptr.hpp"
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace editor;
|
||||
using platform::tests_support::ScopedFile;
|
||||
|
||||
void CheckGeneralTags(pugi::xml_document const & doc)
|
||||
{
|
||||
auto const types = doc.select_nodes("/comaps/editor/types");
|
||||
TEST(!types.empty(), ());
|
||||
auto const fields = doc.select_nodes("/comaps/editor/fields");
|
||||
TEST(!fields.empty(), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(ConfigLoader_Base)
|
||||
{
|
||||
base::AtomicSharedPtr<EditorConfig> config;
|
||||
ConfigLoader loader(config);
|
||||
|
||||
TEST(!config.Get()->GetTypesThatCanBeAdded().empty(), ());
|
||||
}
|
||||
|
||||
// This functionality is not used and corresponding server is not working.
|
||||
// Uncomment it when server will be up.
|
||||
// UNIT_TEST(ConfigLoader_GetRemoteHash)
|
||||
//{
|
||||
// auto const hashStr = ConfigLoader::GetRemoteHash();
|
||||
// TEST_NOT_EQUAL(hashStr, "", ());
|
||||
// TEST_EQUAL(hashStr, ConfigLoader::GetRemoteHash(), ());
|
||||
//}
|
||||
//
|
||||
// UNIT_TEST(ConfigLoader_GetRemoteConfig)
|
||||
//{
|
||||
// pugi::xml_document doc;
|
||||
// ConfigLoader::GetRemoteConfig(doc);
|
||||
// CheckGeneralTags(doc);
|
||||
//}
|
||||
|
||||
UNIT_TEST(ConfigLoader_LoadFromLocal)
|
||||
{
|
||||
pugi::xml_document doc;
|
||||
ConfigLoader::LoadFromLocal(doc);
|
||||
CheckGeneralTags(doc);
|
||||
}
|
||||
} // namespace
|
||||
86
libs/editor/editor_tests/editor_config_test.cpp
Normal file
86
libs/editor/editor_tests/editor_config_test.cpp
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "editor/config_loader.hpp"
|
||||
#include "editor/editor_config.hpp"
|
||||
|
||||
#include "base/stl_helpers.hpp"
|
||||
|
||||
UNIT_TEST(EditorConfig_TypeDescription)
|
||||
{
|
||||
using EType = feature::Metadata::EType;
|
||||
using Fields = editor::TypeAggregatedDescription::FeatureFields;
|
||||
|
||||
Fields const poiInternet = {
|
||||
EType::FMD_OPEN_HOURS, EType::FMD_PHONE_NUMBER, EType::FMD_WEBSITE, EType::FMD_INTERNET,
|
||||
EType::FMD_EMAIL, EType::FMD_LEVEL, EType::FMD_CONTACT_FACEBOOK, EType::FMD_CONTACT_INSTAGRAM,
|
||||
EType::FMD_CONTACT_TWITTER, EType::FMD_CONTACT_VK, EType::FMD_CONTACT_LINE, EType::FMD_CONTACT_FEDIVERSE,
|
||||
EType::FMD_CONTACT_BLUESKY,
|
||||
};
|
||||
|
||||
pugi::xml_document doc;
|
||||
editor::ConfigLoader::LoadFromLocal(doc);
|
||||
|
||||
editor::EditorConfig config;
|
||||
config.SetConfig(doc);
|
||||
|
||||
{
|
||||
editor::TypeAggregatedDescription desc;
|
||||
TEST(!config.GetTypeDescription({"death-star"}, desc), ());
|
||||
}
|
||||
{
|
||||
editor::TypeAggregatedDescription desc;
|
||||
TEST(config.GetTypeDescription({"amenity-hunting_stand"}, desc), ());
|
||||
TEST(desc.IsNameEditable(), ());
|
||||
TEST(!desc.IsAddressEditable(), ());
|
||||
TEST_EQUAL(desc.GetEditableFields(), Fields{EType::FMD_HEIGHT}, ());
|
||||
}
|
||||
{
|
||||
editor::TypeAggregatedDescription desc;
|
||||
TEST(config.GetTypeDescription({"shop-toys"}, desc), ());
|
||||
TEST(desc.IsNameEditable(), ());
|
||||
TEST(desc.IsAddressEditable(), ());
|
||||
TEST_EQUAL(desc.GetEditableFields(), poiInternet, ());
|
||||
}
|
||||
{
|
||||
// Test that amenity-bank is selected as it goes first in config.
|
||||
editor::TypeAggregatedDescription desc;
|
||||
TEST(config.GetTypeDescription({"amenity-bicycle_rental", "amenity-bank"}, desc), ());
|
||||
TEST(desc.IsNameEditable(), ());
|
||||
TEST(desc.IsAddressEditable(), ());
|
||||
TEST_EQUAL(desc.GetEditableFields(), poiInternet, ());
|
||||
}
|
||||
{
|
||||
// Testing type inheritance
|
||||
editor::TypeAggregatedDescription desc;
|
||||
TEST(config.GetTypeDescription({"amenity-place_of_worship-christian"}, desc), ());
|
||||
TEST(desc.IsNameEditable(), ());
|
||||
TEST_EQUAL(desc.GetEditableFields(), poiInternet, ());
|
||||
}
|
||||
{
|
||||
// Testing long type inheritance on a fake object
|
||||
editor::TypeAggregatedDescription desc;
|
||||
TEST(config.GetTypeDescription({"tourism-artwork-impresionism-monet"}, desc), ());
|
||||
TEST(desc.IsNameEditable(), ());
|
||||
TEST_EQUAL(desc.GetEditableFields(), Fields{}, ());
|
||||
}
|
||||
// TODO(mgsergio): Test case with priority="high" when there is one on editor.config.
|
||||
}
|
||||
|
||||
UNIT_TEST(EditorConfig_GetTypesThatCanBeAdded)
|
||||
{
|
||||
pugi::xml_document doc;
|
||||
editor::ConfigLoader::LoadFromLocal(doc);
|
||||
|
||||
editor::EditorConfig config;
|
||||
config.SetConfig(doc);
|
||||
|
||||
auto const types = config.GetTypesThatCanBeAdded();
|
||||
// A sample addable type.
|
||||
TEST(find(begin(types), end(types), "amenity-cafe") != end(types), ());
|
||||
// A sample line type.
|
||||
TEST(find(begin(types), end(types), "highway-primary") == end(types), ());
|
||||
// A sample type marked as can_add="no".
|
||||
TEST(find(begin(types), end(types), "landuse-cemetery") == end(types), ());
|
||||
// A sample type marked as editable="no".
|
||||
TEST(find(begin(types), end(types), "aeroway-airport") == end(types), ());
|
||||
}
|
||||
39
libs/editor/editor_tests/editor_notes_test.cpp
Normal file
39
libs/editor/editor_tests/editor_notes_test.cpp
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "editor/editor_notes.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "platform/platform_tests_support/scoped_file.hpp"
|
||||
|
||||
#include "base/file_name_utils.hpp"
|
||||
#include "base/math.hpp"
|
||||
|
||||
using namespace editor;
|
||||
using platform::tests_support::ScopedFile;
|
||||
|
||||
UNIT_TEST(Notes_Smoke)
|
||||
{
|
||||
auto const fileName = "notes.xml";
|
||||
auto const fullFileName = base::JoinPath(GetPlatform().WritableDir(), fileName);
|
||||
ScopedFile sf(fileName, ScopedFile::Mode::DoNotCreate);
|
||||
{
|
||||
auto const notes = Notes::MakeNotes(fullFileName, true);
|
||||
notes->CreateNote(mercator::ToLatLon({1, 2}), "Some note1");
|
||||
notes->CreateNote(mercator::ToLatLon({2, 2}), "Some note2");
|
||||
notes->CreateNote(mercator::ToLatLon({1, 1}), "Some note3");
|
||||
}
|
||||
{
|
||||
auto const notes = Notes::MakeNotes(fullFileName, true);
|
||||
auto const result = notes->GetNotes();
|
||||
TEST_EQUAL(result.size(), 3, ());
|
||||
std::vector<Note> const expected{{mercator::ToLatLon({1, 2}), "Some note1"},
|
||||
{mercator::ToLatLon({2, 2}), "Some note2"},
|
||||
{mercator::ToLatLon({1, 1}), "Some note3"}};
|
||||
|
||||
auto const isEqual =
|
||||
std::equal(result.begin(), result.end(), expected.begin(), [](Note const & lhs, Note const & rhs)
|
||||
{ return lhs.m_point.EqualDxDy(rhs.m_point, Notes::kTolerance); });
|
||||
TEST(isEqual, ());
|
||||
}
|
||||
}
|
||||
530
libs/editor/editor_tests/feature_matcher_test.cpp
Normal file
530
libs/editor/editor_tests/feature_matcher_test.cpp
Normal file
|
|
@ -0,0 +1,530 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "editor/feature_matcher.hpp"
|
||||
#include "editor/xml_feature.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace
|
||||
{
|
||||
char const * const osmRawResponseWay = R"SEP(
|
||||
<osm version="0.6" generator="CGImap 0.4.0 (22123 thorn-03.openstreetmap.org)" copyright="OpenStreetMap and contributors">
|
||||
<bounds minlat="53.8976570" minlon="27.5576615" maxlat="53.8976570" maxlon="27.5576615"/>
|
||||
<node id="277171984" visible="true" version="2" changeset="20577443" timestamp="2014-02-15T14:37:39Z" user="iglezz" uid="450366" lat="53.8978034" lon="27.5577642"/>
|
||||
<node id="277171986" visible="true" version="2" changeset="20577443" timestamp="2014-02-15T14:37:39Z" user="iglezz" uid="450366" lat="53.8978710" lon="27.5576815"/>
|
||||
<node id="2673014345" visible="true" version="1" changeset="20577443" timestamp="2014-02-15T14:37:11Z" user="iglezz" uid="450366" lat="53.8977652" lon="27.5578039"/>
|
||||
<node id="277171999" visible="true" version="2" changeset="20577443" timestamp="2014-02-15T14:37:39Z" user="iglezz" uid="450366" lat="53.8977484" lon="27.5573596"/>
|
||||
<node id="277172019" visible="true" version="2" changeset="20577443" timestamp="2014-02-15T14:37:39Z" user="iglezz" uid="450366" lat="53.8977254" lon="27.5578377"/>
|
||||
<node id="277172022" visible="true" version="2" changeset="20577443" timestamp="2014-02-15T14:37:39Z" user="iglezz" uid="450366" lat="53.8976041" lon="27.5575181"/>
|
||||
<node id="277172096" visible="true" version="5" changeset="20577443" timestamp="2014-02-15T14:37:39Z" user="iglezz" uid="450366" lat="53.8972383" lon="27.5581521"/>
|
||||
<node id="277172108" visible="true" version="5" changeset="20577443" timestamp="2014-02-15T14:37:39Z" user="iglezz" uid="450366" lat="53.8971731" lon="27.5579751"/>
|
||||
<node id="420748954" visible="true" version="2" changeset="20577443" timestamp="2014-02-15T14:37:42Z" user="iglezz" uid="450366" lat="53.8973934" lon="27.5579876"/>
|
||||
<node id="420748956" visible="true" version="2" changeset="20577443" timestamp="2014-02-15T14:37:42Z" user="iglezz" uid="450366" lat="53.8976570" lon="27.5576615"/>
|
||||
<node id="420748957" visible="true" version="2" changeset="20577443" timestamp="2014-02-15T14:37:42Z" user="iglezz" uid="450366" lat="53.8973811" lon="27.5579541"/>
|
||||
<way id="25432677" visible="true" version="16" changeset="36051033" timestamp="2015-12-19T17:36:15Z" user="i+f" uid="3070773">
|
||||
<nd ref="277171999"/>
|
||||
<nd ref="277171986"/>
|
||||
<nd ref="277171984"/>
|
||||
<nd ref="2673014345"/>
|
||||
<nd ref="277172019"/>
|
||||
<nd ref="420748956"/>
|
||||
<nd ref="277172022"/>
|
||||
<nd ref="277171999"/>
|
||||
<tag k="addr:housenumber" v="16"/>
|
||||
<tag k="addr:postcode" v="220030"/>
|
||||
<tag k="addr:street" v="улица Карла Маркса"/>
|
||||
<tag k="building" v="yes"/>
|
||||
<tag k="name" v="Беллесбумпром"/>
|
||||
<tag k="office" v="company"/>
|
||||
<tag k="website" v="bellesbumprom.by"/>
|
||||
</way>
|
||||
<way id="35995664" visible="true" version="7" changeset="35318978" timestamp="2015-11-14T23:39:54Z" user="osm-belarus" uid="86479">
|
||||
<nd ref="420748956"/>
|
||||
<nd ref="420748957"/>
|
||||
<nd ref="420748954"/>
|
||||
<nd ref="277172096"/>
|
||||
<nd ref="277172108"/>
|
||||
<nd ref="277172022"/>
|
||||
<nd ref="420748956"/>
|
||||
<tag k="addr:housenumber" v="31"/>
|
||||
<tag k="addr:postcode" v="220030"/>
|
||||
<tag k="addr:street" v="Комсомольская улица"/>
|
||||
<tag k="building" v="residential"/>
|
||||
</way>
|
||||
</osm>
|
||||
)SEP";
|
||||
|
||||
char const * const osmRawResponseNode = R"SEP(
|
||||
<osm version="0.6" generator="CGImap 0.4.0 (5501 thorn-02.openstreetmap.org)" copyright="OpenStreetMap and contributors">
|
||||
<bounds minlat="53.8977000" minlon="27.5578900" maxlat="53.8977700" maxlon="27.5579800"/>
|
||||
<node id="2673014342" visible="true" version="1" changeset="20577443" timestamp="2014-02-15T14:37:11Z" user="iglezz" uid="450366" lat="53.8976095" lon="27.5579360"/>
|
||||
<node id="2673014343" visible="true" version="1" changeset="20577443" timestamp="2014-02-15T14:37:11Z" user="iglezz" uid="450366" lat="53.8977023" lon="27.5582512"/>
|
||||
<node id="2673014344" visible="true" version="1" changeset="20577443" timestamp="2014-02-15T14:37:11Z" user="iglezz" uid="450366" lat="53.8977366" lon="27.5579628"/>
|
||||
<node id="2673014345" visible="true" version="1" changeset="20577443" timestamp="2014-02-15T14:37:11Z" user="iglezz" uid="450366" lat="53.8977652" lon="27.5578039"/>
|
||||
<node id="2673014347" visible="true" version="1" changeset="20577443" timestamp="2014-02-15T14:37:11Z" user="iglezz" uid="450366" lat="53.8977970" lon="27.5579116"/>
|
||||
<node id="2673014349" visible="true" version="1" changeset="20577443" timestamp="2014-02-15T14:37:11Z" user="iglezz" uid="450366" lat="53.8977977" lon="27.5581702"/>
|
||||
<node id="277172019" visible="true" version="2" changeset="20577443" timestamp="2014-02-15T14:37:39Z" user="iglezz" uid="450366" lat="53.8977254" lon="27.5578377"/>
|
||||
<node id="3900254358" visible="true" version="1" changeset="36051033" timestamp="2015-12-19T17:36:14Z" user="i+f" uid="3070773" lat="53.8977398" lon="27.5579251">
|
||||
<tag k="name" v="Главное управление капитального строительства"/>
|
||||
<tag k="office" v="company"/>
|
||||
<tag k="website" v="guks.by"/>
|
||||
</node>
|
||||
<way id="261703253" visible="true" version="2" changeset="35318978" timestamp="2015-11-14T23:30:51Z" user="osm-belarus" uid="86479">
|
||||
<nd ref="2673014345"/>
|
||||
<nd ref="2673014347"/>
|
||||
<nd ref="2673014344"/>
|
||||
<nd ref="2673014349"/>
|
||||
<nd ref="2673014343"/>
|
||||
<nd ref="2673014342"/>
|
||||
<nd ref="277172019"/>
|
||||
<nd ref="2673014345"/>
|
||||
<tag k="addr:housenumber" v="16"/>
|
||||
<tag k="addr:postcode" v="220030"/>
|
||||
<tag k="addr:street" v="улица Карла Маркса"/>
|
||||
<tag k="building" v="residential"/>
|
||||
</way>
|
||||
</osm>
|
||||
)SEP";
|
||||
|
||||
char const * const osmRawResponseRelation = R"SEP(
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<osm version="0.6" generator="CGImap 0.4.0 (22560 thorn-01.openstreetmap.org)" copyright="OpenStreetMap and contributors">
|
||||
<bounds minlat="55.7509200" minlon="37.6397200" maxlat="55.7515400" maxlon="37.6411300"/>
|
||||
<node id="271892032" visible="true" version="4" changeset="8261156" timestamp="2011-05-27T10:18:35Z" user="Scondo" uid="421524" lat="55.7524913" lon="37.6397264"/>
|
||||
<node id="271892033" visible="true" version="4" changeset="8261156" timestamp="2011-05-27T10:18:17Z" user="Scondo" uid="421524" lat="55.7522475" lon="37.6391447"/>
|
||||
<node id="583193392" visible="true" version="2" changeset="4128036" timestamp="2010-03-14T18:31:07Z" user="Vovanium" uid="87682" lat="55.7507909" lon="37.6404902"/>
|
||||
<node id="583193432" visible="true" version="3" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7510964" lon="37.6397197"/>
|
||||
<node id="583193426" visible="true" version="3" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7510560" lon="37.6394035"/>
|
||||
<node id="583193429" visible="true" version="3" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7512865" lon="37.6396919"/>
|
||||
<node id="583193395" visible="true" version="2" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7509787" lon="37.6401799"/>
|
||||
<node id="583193415" visible="true" version="3" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7510898" lon="37.6403571"/>
|
||||
<node id="583193424" visible="true" version="3" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7508581" lon="37.6399029"/>
|
||||
<node id="583193422" visible="true" version="3" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7509689" lon="37.6400415"/>
|
||||
<node id="583193398" visible="true" version="2" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7514775" lon="37.6401937"/>
|
||||
<node id="583193416" visible="true" version="3" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7513532" lon="37.6405069"/>
|
||||
<node id="583193431" visible="true" version="3" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7512162" lon="37.6398695"/>
|
||||
<node id="583193390" visible="true" version="2" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7507783" lon="37.6410989"/>
|
||||
<node id="583193388" visible="true" version="2" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7509982" lon="37.6416194"/>
|
||||
<node id="583193405" visible="true" version="3" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7514149" lon="37.6406910"/>
|
||||
<node id="583193408" visible="true" version="2" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7509930" lon="37.6412441"/>
|
||||
<node id="583193410" visible="true" version="3" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7509124" lon="37.6406648"/>
|
||||
<node id="583193401" visible="true" version="2" changeset="4128036" timestamp="2010-03-14T18:31:09Z" user="Vovanium" uid="87682" lat="55.7516648" lon="37.6407506"/>
|
||||
<node id="666179513" visible="true" version="1" changeset="4128036" timestamp="2010-03-14T18:30:48Z" user="Vovanium" uid="87682" lat="55.7510740" lon="37.6401059"/>
|
||||
<node id="666179517" visible="true" version="1" changeset="4128036" timestamp="2010-03-14T18:30:48Z" user="Vovanium" uid="87682" lat="55.7511507" lon="37.6403206"/>
|
||||
<node id="666179519" visible="true" version="1" changeset="4128036" timestamp="2010-03-14T18:30:48Z" user="Vovanium" uid="87682" lat="55.7512863" lon="37.6403782"/>
|
||||
<node id="666179521" visible="true" version="1" changeset="4128036" timestamp="2010-03-14T18:30:48Z" user="Vovanium" uid="87682" lat="55.7512185" lon="37.6403206"/>
|
||||
<node id="666179522" visible="true" version="1" changeset="4128036" timestamp="2010-03-14T18:30:48Z" user="Vovanium" uid="87682" lat="55.7512214" lon="37.6400483"/>
|
||||
<node id="666179524" visible="true" version="1" changeset="4128036" timestamp="2010-03-14T18:30:48Z" user="Vovanium" uid="87682" lat="55.7513393" lon="37.6401111"/>
|
||||
<node id="666179526" visible="true" version="1" changeset="4128036" timestamp="2010-03-14T18:30:48Z" user="Vovanium" uid="87682" lat="55.7514337" lon="37.6402525"/>
|
||||
<node id="666179528" visible="true" version="1" changeset="4128036" timestamp="2010-03-14T18:30:48Z" user="Vovanium" uid="87682" lat="55.7507439" lon="37.6406349"/>
|
||||
<node id="666179530" visible="true" version="1" changeset="4128036" timestamp="2010-03-14T18:30:48Z" user="Vovanium" uid="87682" lat="55.7507291" lon="37.6407868"/>
|
||||
<node id="666179531" visible="true" version="1" changeset="4128036" timestamp="2010-03-14T18:30:48Z" user="Vovanium" uid="87682" lat="55.7507380" lon="37.6409544"/>
|
||||
<node id="666179540" visible="true" version="1" changeset="4128036" timestamp="2010-03-14T18:30:48Z" user="Vovanium" uid="87682" lat="55.7508736" lon="37.6408287"/>
|
||||
<node id="595699492" visible="true" version="2" changeset="4128036" timestamp="2010-03-14T18:31:08Z" user="Vovanium" uid="87682" lat="55.7508900" lon="37.6410015"/>
|
||||
<node id="271892037" visible="true" version="4" changeset="8261156" timestamp="2011-05-27T10:18:03Z" user="Scondo" uid="421524" lat="55.7519689" lon="37.6393462"/>
|
||||
<node id="666179544" visible="true" version="2" changeset="8261156" timestamp="2011-05-27T10:18:03Z" user="Scondo" uid="421524" lat="55.7523858" lon="37.6394615"/>
|
||||
<node id="271892040" visible="true" version="4" changeset="8261156" timestamp="2011-05-27T10:18:09Z" user="Scondo" uid="421524" lat="55.7518044" lon="37.6401900"/>
|
||||
<node id="271892039" visible="true" version="4" changeset="8261156" timestamp="2011-05-27T10:18:11Z" user="Scondo" uid="421524" lat="55.7518997" lon="37.6400631"/>
|
||||
<node id="271892031" visible="true" version="4" changeset="8261156" timestamp="2011-05-27T10:18:23Z" user="Scondo" uid="421524" lat="55.7517772" lon="37.6406618"/>
|
||||
<node id="271892036" visible="true" version="4" changeset="8261156" timestamp="2011-05-27T10:18:23Z" user="Scondo" uid="421524" lat="55.7521424" lon="37.6397730"/>
|
||||
<node id="271892035" visible="true" version="4" changeset="8261156" timestamp="2011-05-27T10:18:25Z" user="Scondo" uid="421524" lat="55.7522520" lon="37.6396264"/>
|
||||
<node id="666179542" visible="true" version="2" changeset="8261156" timestamp="2011-05-27T10:18:26Z" user="Scondo" uid="421524" lat="55.7523415" lon="37.6393631"/>
|
||||
<node id="271892038" visible="true" version="4" changeset="8261156" timestamp="2011-05-27T10:18:30Z" user="Scondo" uid="421524" lat="55.7517353" lon="37.6396389"/>
|
||||
<node id="666179545" visible="true" version="2" changeset="8261156" timestamp="2011-05-27T10:18:30Z" user="Scondo" uid="421524" lat="55.7523947" lon="37.6392844"/>
|
||||
<node id="271892041" visible="true" version="4" changeset="8261156" timestamp="2011-05-27T10:18:34Z" user="Scondo" uid="421524" lat="55.7516804" lon="37.6398672"/>
|
||||
<node id="666179548" visible="true" version="2" changeset="8261156" timestamp="2011-05-27T10:18:35Z" user="Scondo" uid="421524" lat="55.7524390" lon="37.6393828"/>
|
||||
<node id="271892030" visible="true" version="4" changeset="8261156" timestamp="2011-05-27T10:18:38Z" user="Scondo" uid="421524" lat="55.7515240" lon="37.6400640"/>
|
||||
<node id="271892034" visible="true" version="4" changeset="8261156" timestamp="2011-05-27T10:18:40Z" user="Scondo" uid="421524" lat="55.7521203" lon="37.6393028"/>
|
||||
<node id="2849850611" visible="true" version="2" changeset="33550372" timestamp="2015-08-24T15:55:36Z" user="vadp" uid="326091" lat="55.7507261" lon="37.6405934"/>
|
||||
<node id="2849850614" visible="true" version="1" changeset="22264538" timestamp="2014-05-11T07:26:43Z" user="Vadim Zudkin" uid="177747" lat="55.7509233" lon="37.6401297"/>
|
||||
<node id="3712207029" visible="true" version="1" changeset="33550372" timestamp="2015-08-24T15:55:35Z" user="vadp" uid="326091" lat="55.7510865" lon="37.6400013"/>
|
||||
<node id="3712207030" visible="true" version="1" changeset="33550372" timestamp="2015-08-24T15:55:35Z" user="vadp" uid="326091" lat="55.7512462" lon="37.6399456"/>
|
||||
<node id="3712207031" visible="true" version="2" changeset="33550412" timestamp="2015-08-24T15:57:10Z" user="vadp" uid="326091" lat="55.7514944" lon="37.6401534"/>
|
||||
<node id="3712207032" visible="true" version="2" changeset="33550412" timestamp="2015-08-24T15:57:10Z" user="vadp" uid="326091" lat="55.7516969" lon="37.6407362">
|
||||
<tag k="access" v="private"/>
|
||||
<tag k="barrier" v="gate"/>
|
||||
</node>
|
||||
<node id="3712207033" visible="true" version="2" changeset="33550412" timestamp="2015-08-24T15:57:10Z" user="vadp" uid="326091" lat="55.7517316" lon="37.6408217"/>
|
||||
<node id="3712207034" visible="true" version="2" changeset="33550412" timestamp="2015-08-24T15:57:10Z" user="vadp" uid="326091" lat="55.7517602" lon="37.6409066"/>
|
||||
<node id="2849850613" visible="true" version="3" changeset="33551686" timestamp="2015-08-24T16:50:21Z" user="vadp" uid="326091" lat="55.7507965" lon="37.6399611"/>
|
||||
<node id="338464706" visible="true" version="3" changeset="33551686" timestamp="2015-08-24T16:50:21Z" user="vadp" uid="326091" lat="55.7510322" lon="37.6393637"/>
|
||||
<node id="338464708" visible="true" version="7" changeset="33551686" timestamp="2015-08-24T16:50:21Z" user="vadp" uid="326091" lat="55.7515407" lon="37.6383137"/>
|
||||
<node id="3755931947" visible="true" version="1" changeset="34206452" timestamp="2015-09-23T13:58:11Z" user="trolleway" uid="397326" lat="55.7517090" lon="37.6407565"/>
|
||||
<way id="25009838" visible="true" version="14" changeset="28090002" timestamp="2015-01-12T16:15:21Z" user="midrug" uid="2417727">
|
||||
<nd ref="271892030"/>
|
||||
<nd ref="271892031"/>
|
||||
<nd ref="271892032"/>
|
||||
<nd ref="666179544"/>
|
||||
<nd ref="666179548"/>
|
||||
<nd ref="666179545"/>
|
||||
<nd ref="666179542"/>
|
||||
<nd ref="271892033"/>
|
||||
<nd ref="271892034"/>
|
||||
<nd ref="271892035"/>
|
||||
<nd ref="271892036"/>
|
||||
<nd ref="271892037"/>
|
||||
<nd ref="271892038"/>
|
||||
<nd ref="271892039"/>
|
||||
<nd ref="271892040"/>
|
||||
<nd ref="271892041"/>
|
||||
<nd ref="271892030"/>
|
||||
<tag k="addr:housenumber" v="12-14"/>
|
||||
<tag k="addr:street" v="улица Солянка"/>
|
||||
<tag k="building" v="yes"/>
|
||||
<tag k="building:colour" v="lightpink"/>
|
||||
<tag k="building:levels" v="3"/>
|
||||
<tag k="description:en" v="Housed the Board of Trustees, a public institution of the Russian Empire until 1917"/>
|
||||
<tag k="end_date" v="1826"/>
|
||||
<tag k="name" v="Опекунский совет"/>
|
||||
<tag k="name:de" v="Kuratorium"/>
|
||||
<tag k="name:en" v="Board of Trustees Building"/>
|
||||
<tag k="ref" v="7710784000"/>
|
||||
<tag k="roof:material" v="metal"/>
|
||||
<tag k="source:description:en" v="wikipedia:ru"/>
|
||||
<tag k="start_date" v="1823"/>
|
||||
<tag k="tourism" v="attraction"/>
|
||||
<tag k="wikipedia" v="ru:Опекунский совет (Москва)"/>
|
||||
</way>
|
||||
<way id="45814282" visible="true" version="3" changeset="4128036" timestamp="2010-03-14T18:31:24Z" user="Vovanium" uid="87682">
|
||||
<nd ref="583193405"/>
|
||||
<nd ref="583193408"/>
|
||||
<nd ref="595699492"/>
|
||||
<nd ref="666179540"/>
|
||||
<nd ref="583193410"/>
|
||||
<nd ref="583193415"/>
|
||||
<nd ref="666179517"/>
|
||||
<nd ref="666179521"/>
|
||||
<nd ref="666179519"/>
|
||||
<nd ref="583193416"/>
|
||||
<nd ref="583193405"/>
|
||||
</way>
|
||||
<way id="109538181" visible="true" version="7" changeset="34206452" timestamp="2015-09-23T13:58:11Z" user="trolleway" uid="397326">
|
||||
<nd ref="338464708"/>
|
||||
<nd ref="338464706"/>
|
||||
<nd ref="2849850613"/>
|
||||
<nd ref="2849850614"/>
|
||||
<nd ref="3712207029"/>
|
||||
<nd ref="3712207030"/>
|
||||
<nd ref="3712207031"/>
|
||||
<nd ref="3712207032"/>
|
||||
<nd ref="3755931947"/>
|
||||
<nd ref="3712207033"/>
|
||||
<nd ref="3712207034"/>
|
||||
<tag k="highway" v="service"/>
|
||||
</way>
|
||||
<way id="45814281" visible="true" version="6" changeset="9583527" timestamp="2011-10-17T16:38:55Z" user="luch86" uid="266092">
|
||||
<nd ref="583193388"/>
|
||||
<nd ref="583193390"/>
|
||||
<nd ref="666179531"/>
|
||||
<nd ref="666179530"/>
|
||||
<nd ref="666179528"/>
|
||||
<nd ref="583193392"/>
|
||||
<nd ref="583193395"/>
|
||||
<nd ref="666179513"/>
|
||||
<nd ref="666179522"/>
|
||||
<nd ref="666179524"/>
|
||||
<nd ref="666179526"/>
|
||||
<nd ref="583193398"/>
|
||||
<nd ref="583193401"/>
|
||||
<nd ref="583193388"/>
|
||||
</way>
|
||||
<way id="45814283" visible="true" version="3" changeset="28090002" timestamp="2015-01-12T16:15:23Z" user="midrug" uid="2417727">
|
||||
<nd ref="583193422"/>
|
||||
<nd ref="583193424"/>
|
||||
<nd ref="583193426"/>
|
||||
<nd ref="583193429"/>
|
||||
<nd ref="583193431"/>
|
||||
<nd ref="583193432"/>
|
||||
<nd ref="583193422"/>
|
||||
<tag k="addr:street" v="улица Солянка"/>
|
||||
<tag k="building" v="yes"/>
|
||||
<tag k="building:colour" v="goldenrod"/>
|
||||
<tag k="building:levels" v="2"/>
|
||||
<tag k="roof:colour" v="black"/>
|
||||
<tag k="roof:material" v="tar_paper"/>
|
||||
</way>
|
||||
<way id="367274913" visible="true" version="2" changeset="33550484" timestamp="2015-08-24T16:00:25Z" user="vadp" uid="326091">
|
||||
<nd ref="2849850614"/>
|
||||
<nd ref="2849850611"/>
|
||||
<tag k="highway" v="service"/>
|
||||
</way>
|
||||
<relation id="365808" visible="true" version="6" changeset="28090002" timestamp="2015-01-12T16:15:14Z" user="midrug" uid="2417727">
|
||||
<member type="way" ref="45814281" role="outer"/>
|
||||
<member type="way" ref="45814282" role="inner"/>
|
||||
<tag k="addr:housenumber" v="14/2"/>
|
||||
<tag k="addr:street" v="улица Солянка"/>
|
||||
<tag k="building" v="yes"/>
|
||||
<tag k="building:colour" v="gold"/>
|
||||
<tag k="building:levels" v="2"/>
|
||||
<tag k="roof:material" v="metal"/>
|
||||
<tag k="type" v="multipolygon"/>
|
||||
</relation>
|
||||
</osm>
|
||||
)SEP";
|
||||
|
||||
// Note: Geometry should not contain duplicates.
|
||||
|
||||
UNIT_TEST(GetBestOsmNode_Test)
|
||||
{
|
||||
{
|
||||
pugi::xml_document osmResponse;
|
||||
TEST(osmResponse.load_buffer(osmRawResponseNode, ::strlen(osmRawResponseNode)), ());
|
||||
|
||||
auto const bestNode = matcher::GetBestOsmNode(osmResponse, ms::LatLon(53.8977398, 27.5579251));
|
||||
TEST_EQUAL(editor::XMLFeature(bestNode).GetName(), "Главное управление капитального строительства", ());
|
||||
}
|
||||
{
|
||||
pugi::xml_document osmResponse;
|
||||
TEST(osmResponse.load_buffer(osmRawResponseNode, ::strlen(osmRawResponseNode)), ());
|
||||
|
||||
auto const bestNode = matcher::GetBestOsmNode(osmResponse, ms::LatLon(53.8977254, 27.5578377));
|
||||
TEST_EQUAL(bestNode.attribute("id").value(), std::string("277172019"), ());
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(GetBestOsmWay_Test)
|
||||
{
|
||||
{
|
||||
pugi::xml_document osmResponse;
|
||||
TEST(osmResponse.load_buffer(osmRawResponseWay, ::strlen(osmRawResponseWay)), ());
|
||||
std::vector<m2::PointD> const geometry = {
|
||||
{27.557515856307106, 64.236609073256034}, {27.55784576801841, 64.236820967769773},
|
||||
{27.557352241556003, 64.236863883114324}, {27.55784576801841, 64.236820967769773},
|
||||
{27.557352241556003, 64.236863883114324}, {27.557765301747366, 64.236963124848614},
|
||||
{27.557352241556003, 64.236863883114324}, {27.557765301747366, 64.236963124848614},
|
||||
{27.55768215326728, 64.237078459837136}};
|
||||
|
||||
auto const bestWay = matcher::GetBestOsmWayOrRelation(osmResponse, geometry);
|
||||
TEST(bestWay, ());
|
||||
TEST_EQUAL(editor::XMLFeature(bestWay).GetName(), "Беллесбумпром", ());
|
||||
}
|
||||
{
|
||||
pugi::xml_document osmResponse;
|
||||
TEST(osmResponse.load_buffer(osmRawResponseWay, ::strlen(osmRawResponseWay)), ());
|
||||
// Each point is moved for 0.0001 on x and y. It is aboout a half of side length.
|
||||
std::vector<m2::PointD> const geometry = {
|
||||
{27.557615856307106, 64.236709073256034}, {27.55794576801841, 64.236920967769773},
|
||||
{27.557452241556003, 64.236963883114324}, {27.55794576801841, 64.236920967769773},
|
||||
{27.557452241556003, 64.236963883114324}, {27.557865301747366, 64.237063124848614},
|
||||
{27.557452241556003, 64.236963883114324}, {27.557865301747366, 64.237063124848614},
|
||||
{27.55778215326728, 64.237178459837136}};
|
||||
|
||||
auto const bestWay = matcher::GetBestOsmWayOrRelation(osmResponse, geometry);
|
||||
TEST(!bestWay, ());
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(GetBestOsmRealtion_Test)
|
||||
{
|
||||
pugi::xml_document osmResponse;
|
||||
TEST(osmResponse.load_buffer(osmRawResponseRelation, ::strlen(osmRawResponseRelation)), ());
|
||||
std::vector<m2::PointD> const geometry = {
|
||||
{37.640253436865322, 67.455316241497655}, {37.64019442826654, 67.455394025559684},
|
||||
{37.640749645536772, 67.455726619480004}, {37.640253436865322, 67.455316241497655},
|
||||
{37.640749645536772, 67.455726619480004}, {37.64069063693799, 67.455281372780206},
|
||||
{37.640253436865322, 67.455316241497655}, {37.64069063693799, 67.455281372780206},
|
||||
{37.640505564514598, 67.455174084418815}, {37.640253436865322, 67.455316241497655},
|
||||
{37.640505564514598, 67.455174084418815}, {37.640376818480917, 67.455053385012263},
|
||||
{37.640253436865322, 67.455316241497655}, {37.640376818480917, 67.455053385012263},
|
||||
{37.640320492091178, 67.454932685605684}, {37.640253436865322, 67.455316241497655},
|
||||
{37.640320492091178, 67.454932685605684}, {37.640111279786453, 67.455147262328467},
|
||||
{37.640111279786453, 67.455147262328467}, {37.640320492091178, 67.454932685605684},
|
||||
{37.640046906769612, 67.454938050023742}, {37.640046906769612, 67.454938050023742},
|
||||
{37.640320492091178, 67.454932685605684}, {37.640320492091178, 67.454811986199104},
|
||||
{37.640046906769612, 67.454938050023742}, {37.640320492091178, 67.454811986199104},
|
||||
{37.640105915368395, 67.454677875747365}, {37.640105915368395, 67.454677875747365},
|
||||
{37.640320492091178, 67.454811986199104}, {37.64035804301767, 67.454704697837713},
|
||||
{37.640105915368395, 67.454677875747365}, {37.64035804301767, 67.454704697837713},
|
||||
{37.64018101722138, 67.454508896578176}, {37.64018101722138, 67.454508896578176},
|
||||
{37.64035804301767, 67.454704697837713}, {37.640663814847642, 67.454390879380639},
|
||||
{37.64018101722138, 67.454508896578176}, {37.640663814847642, 67.454390879380639},
|
||||
{37.640489471260366, 67.454173620448813}, {37.640489471260366, 67.454173620448813},
|
||||
{37.640663814847642, 67.454390879380639}, {37.640634310548251, 67.454090471968726},
|
||||
{37.640634310548251, 67.454090471968726}, {37.640663814847642, 67.454390879380639},
|
||||
{37.640827429598772, 67.454321141945741}, {37.640634310548251, 67.454090471968726},
|
||||
{37.640827429598772, 67.454321141945741}, {37.640787196463265, 67.454063649878378},
|
||||
{37.640787196463265, 67.454063649878378}, {37.640827429598772, 67.454321141945741},
|
||||
{37.64095349342341, 67.454079743132581}, {37.64095349342341, 67.454079743132581},
|
||||
{37.640827429598772, 67.454321141945741}, {37.641001773186048, 67.454350646245103},
|
||||
{37.64095349342341, 67.454079743132581}, {37.641001773186048, 67.454350646245103},
|
||||
{37.641098332711294, 67.454152162776523}, {37.641098332711294, 67.454152162776523},
|
||||
{37.641001773186048, 67.454350646245103}, {37.641243171999179, 67.45453303645948},
|
||||
{37.641098332711294, 67.454152162776523}, {37.641243171999179, 67.45453303645948},
|
||||
{37.641618681264049, 67.454541083086582}, {37.641618681264049, 67.454541083086582},
|
||||
{37.641243171999179, 67.45453303645948}, {37.64069063693799, 67.455281372780206},
|
||||
{37.641618681264049, 67.454541083086582}, {37.64069063693799, 67.455281372780206},
|
||||
{37.640749645536772, 67.455726619480004}};
|
||||
|
||||
auto const bestWay = matcher::GetBestOsmWayOrRelation(osmResponse, geometry);
|
||||
TEST_EQUAL(bestWay.attribute("id").value(), std::string("365808"), ());
|
||||
}
|
||||
|
||||
char const * const osmResponseBuildingMiss = R"SEP(
|
||||
<osm version="0.6" generator="CGImap 0.4.0 (8662 thorn-01.openstreetmap.org)">
|
||||
<bounds minlat="51.5342700" minlon="-0.2047000" maxlat="51.5343200" maxlon="-0.2046300"/>
|
||||
<node id="861357349" visible="true" version="3" changeset="31214483" timestamp="2015-05-16T23:10:03Z" user="Derick Rethans" uid="37137" lat="51.5342451" lon="-0.2046356"/>
|
||||
<node id="3522706827" visible="true" version="1" changeset="31214483" timestamp="2015-05-16T23:09:47Z" user="Derick Rethans" uid="37137" lat="51.5342834" lon="-0.2046544">
|
||||
<tag k="addr:housenumber" v="26a"/>
|
||||
<tag k="addr:street" v="Salusbury Road"/>
|
||||
</node>
|
||||
<node id="3522707171" visible="true" version="1" changeset="31214483" timestamp="2015-05-16T23:09:50Z" user="Derick Rethans" uid="37137" lat="51.5342161" lon="-0.2047884"/>
|
||||
<node id="3522707175" visible="true" version="1" changeset="31214483" timestamp="2015-05-16T23:09:50Z" user="Derick Rethans" uid="37137" lat="51.5342627" lon="-0.2048113"/>
|
||||
<node id="3522707179" visible="true" version="1" changeset="31214483" timestamp="2015-05-16T23:09:50Z" user="Derick Rethans" uid="37137" lat="51.5342918" lon="-0.2046585"/>
|
||||
<node id="3522707180" visible="true" version="1" changeset="31214483" timestamp="2015-05-16T23:09:50Z" user="Derick Rethans" uid="37137" lat="51.5343060" lon="-0.2048326"/>
|
||||
<node id="3522707185" visible="true" version="1" changeset="31214483" timestamp="2015-05-16T23:09:50Z" user="Derick Rethans" uid="37137" lat="51.5343350" lon="-0.2046798"/>
|
||||
<way id="345630057" visible="true" version="3" changeset="38374962" timestamp="2016-04-07T09:19:02Z" user="Derick Rethans" uid="37137">
|
||||
<nd ref="3522707179"/>
|
||||
<nd ref="3522707185"/>
|
||||
<nd ref="3522707180"/>
|
||||
<nd ref="3522707175"/>
|
||||
<nd ref="3522707179"/>
|
||||
<tag k="addr:housenumber" v="26"/>
|
||||
<tag k="addr:street" v="Salusbury Road"/>
|
||||
<tag k="building" v="yes"/>
|
||||
<tag k="building:levels" v="1"/>
|
||||
<tag k="name" v="Londis"/>
|
||||
<tag k="shop" v="convenience"/>
|
||||
</way>
|
||||
<way id="345630019" visible="true" version="2" changeset="38374962" timestamp="2016-04-07T09:19:02Z" user="Derick Rethans" uid="37137">
|
||||
<nd ref="861357349"/>
|
||||
<nd ref="3522706827"/>
|
||||
<nd ref="3522707179"/>
|
||||
<nd ref="3522707175"/>
|
||||
<nd ref="3522707171"/>
|
||||
<nd ref="861357349"/>
|
||||
<tag k="addr:housenumber" v="26"/>
|
||||
<tag k="addr:street" v="Salusbury Road"/>
|
||||
<tag k="building" v="yes"/>
|
||||
<tag k="building:levels" v="2"/>
|
||||
<tag k="name" v="Shampoo Hair Salon"/>
|
||||
<tag k="shop" v="hairdresser"/>
|
||||
</way>
|
||||
</osm>
|
||||
)SEP";
|
||||
|
||||
UNIT_TEST(HouseBuildingMiss_test)
|
||||
{
|
||||
pugi::xml_document osmResponse;
|
||||
TEST(osmResponse.load_buffer(osmResponseBuildingMiss, ::strlen(osmResponseBuildingMiss)), ());
|
||||
std::vector<m2::PointD> const geometry = {
|
||||
{-0.2048121407986514, 60.333984198674443}, {-0.20478800091734684, 60.333909096821458},
|
||||
{-0.20465925488366565, 60.334029796228037}, {-0.2048121407986514, 60.333984198674443},
|
||||
{-0.20478800091734684, 60.333909096821458}, {-0.20463511500236109, 60.333954694375052}};
|
||||
|
||||
auto const bestWay = matcher::GetBestOsmWayOrRelation(osmResponse, geometry);
|
||||
TEST_EQUAL(bestWay.attribute("id").value(), std::string("345630019"), ());
|
||||
}
|
||||
|
||||
std::string const kHouseWithSeveralEntrances = R"xxx("
|
||||
<osm version="0.6" generator="CGImap 0.6.0 (3589 thorn-03.openstreetmap.org)" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
|
||||
<node id="339283610" visible="true" version="6" changeset="33699414" timestamp="2015-08-31T09:53:02Z" user="Lazy Ranma" uid="914471" lat="55.8184397" lon="37.5700770"/>
|
||||
<node id="339283612" visible="true" version="6" changeset="33699414" timestamp="2015-08-31T09:53:02Z" user="Lazy Ranma" uid="914471" lat="55.8184655" lon="37.5702599"/>
|
||||
<node id="339283614" visible="true" version="6" changeset="33699414" timestamp="2015-08-31T09:53:02Z" user="Lazy Ranma" uid="914471" lat="55.8190524" lon="37.5698027"/>
|
||||
<node id="339283615" visible="true" version="6" changeset="33699414" timestamp="2015-08-31T09:53:02Z" user="Lazy Ranma" uid="914471" lat="55.8190782" lon="37.5699856"/>
|
||||
<node id="1131238558" visible="true" version="7" changeset="33699414" timestamp="2015-08-31T09:52:50Z" user="Lazy Ranma" uid="914471" lat="55.8188226" lon="37.5699055">
|
||||
<tag k="entrance" v="yes"/>
|
||||
<tag k="ref" v="2"/>
|
||||
</node>
|
||||
<node id="1131238581" visible="true" version="7" changeset="33699414" timestamp="2015-08-31T09:52:51Z" user="Lazy Ranma" uid="914471" lat="55.8185163" lon="37.5700427">
|
||||
<tag k="entrance" v="yes"/>
|
||||
<tag k="ref" v="4"/>
|
||||
</node>
|
||||
<node id="1131238623" visible="true" version="7" changeset="33699414" timestamp="2015-08-31T09:52:51Z" user="Lazy Ranma" uid="914471" lat="55.8189758" lon="37.5698370">
|
||||
<tag k="entrance" v="yes"/>
|
||||
<tag k="ref" v="1"/>
|
||||
</node>
|
||||
<node id="1131238704" visible="true" version="7" changeset="33699414" timestamp="2015-08-31T09:52:52Z" user="Lazy Ranma" uid="914471" lat="55.8186694" lon="37.5699741">
|
||||
<tag k="entrance" v="yes"/>
|
||||
<tag k="ref" v="3"/>
|
||||
</node>
|
||||
<way id="30680719" visible="true" version="10" changeset="25301783" timestamp="2014-09-08T07:52:43Z" user="Felis Pimeja" uid="260756">
|
||||
<nd ref="339283614"/>
|
||||
<nd ref="339283615"/>
|
||||
<nd ref="339283612"/>
|
||||
<nd ref="339283610"/>
|
||||
<nd ref="1131238581"/>
|
||||
<nd ref="1131238704"/>
|
||||
<nd ref="1131238558"/>
|
||||
<nd ref="1131238623"/>
|
||||
<nd ref="339283614"/>
|
||||
<tag k="addr:city" v="Москва"/>
|
||||
<tag k="addr:country" v="RU"/>
|
||||
<tag k="addr:housenumber" v="14 к1"/>
|
||||
<tag k="addr:street" v="Ивановская улица"/>
|
||||
<tag k="building" v="yes"/>
|
||||
</way>
|
||||
</osm>
|
||||
)xxx";
|
||||
|
||||
UNIT_TEST(HouseWithSeveralEntrances)
|
||||
{
|
||||
pugi::xml_document osmResponse;
|
||||
TEST(osmResponse.load_buffer(kHouseWithSeveralEntrances.c_str(), kHouseWithSeveralEntrances.size()), ());
|
||||
|
||||
std::vector<m2::PointD> geometry = {
|
||||
{37.570076119676798, 67.574481424499169}, {37.570258509891175, 67.574527022052763},
|
||||
{37.569802534355233, 67.575570401367315}, {37.570258509891175, 67.574527022052763},
|
||||
{37.569802534355233, 67.575570401367315}, {37.56998492456961, 67.57561599892091}};
|
||||
|
||||
auto const bestWay = matcher::GetBestOsmWayOrRelation(osmResponse, geometry);
|
||||
TEST_EQUAL(bestWay.attribute("id").value(), std::string("30680719"), ());
|
||||
}
|
||||
|
||||
std::string const kRelationWithSingleWay = R"XXX(
|
||||
<osm version="0.6" generator="CGImap 0.6.0 (2191 thorn-01.openstreetmap.org)" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
|
||||
<node id="287253844" visible="true" version="6" changeset="41744272" timestamp="2016-08-27T20:57:16Z" user="Vadiм" uid="326091" lat="55.7955735" lon="37.5399204"/>
|
||||
<node id="287253846" visible="true" version="6" changeset="41744272" timestamp="2016-08-27T20:57:16Z" user="Vadiм" uid="326091" lat="55.7952597" lon="37.5394774"/>
|
||||
<node id="287253848" visible="true" version="6" changeset="41744272" timestamp="2016-08-27T20:57:16Z" user="Vadiм" uid="326091" lat="55.7947419" lon="37.5406379"/>
|
||||
<node id="287253850" visible="true" version="7" changeset="41744272" timestamp="2016-08-27T20:57:16Z" user="Vadiм" uid="326091" lat="55.7950556" lon="37.5410809"/>
|
||||
<node id="3481651579" visible="true" version="2" changeset="41744272" timestamp="2016-08-27T20:57:16Z" user="Vadiм" uid="326091" lat="55.7946855" lon="37.5407643"/>
|
||||
<node id="3481651621" visible="true" version="2" changeset="41744272" timestamp="2016-08-27T20:57:16Z" user="Vadiм" uid="326091" lat="55.7949992" lon="37.5412073"/>
|
||||
<node id="4509122916" visible="true" version="1" changeset="43776534" timestamp="2016-11-18T19:35:01Z" user="aq123" uid="4289510" lat="55.7954158" lon="37.5396978">
|
||||
<tag k="entrance" v="main"/>
|
||||
</node>
|
||||
<way id="26232961" visible="true" version="10" changeset="43776534" timestamp="2016-11-18T19:35:01Z" user="aq123" uid="4289510">
|
||||
<nd ref="287253844"/>
|
||||
<nd ref="4509122916"/>
|
||||
<nd ref="287253846"/>
|
||||
<nd ref="287253848"/>
|
||||
<nd ref="3481651579"/>
|
||||
<nd ref="3481651621"/>
|
||||
<nd ref="287253850"/>
|
||||
<nd ref="287253844"/>
|
||||
<tag k="building" v="yes"/>
|
||||
<tag k="leisure" v="sports_centre"/>
|
||||
</way>
|
||||
<relation id="111" visible="true" version="6" changeset="222" timestamp="2015-01-12T16:15:14Z" user="test" uid="333">
|
||||
<member type="way" ref="26232961"/>
|
||||
<tag k="building" v="yes"/>
|
||||
<tag k="type" v="multipolygon"/>
|
||||
</relation>
|
||||
</osm>
|
||||
)XXX";
|
||||
|
||||
UNIT_TEST(RelationWithSingleWay)
|
||||
{
|
||||
pugi::xml_document osmResponse;
|
||||
TEST(osmResponse.load_buffer(kRelationWithSingleWay.c_str(), kRelationWithSingleWay.size()), ());
|
||||
|
||||
std::vector<m2::PointD> geometry = {
|
||||
{37.539920043497688, 67.533792313440074}, {37.539477479006933, 67.533234413960827},
|
||||
{37.541207503834414, 67.532770391797811}, {37.539477479006933, 67.533234413960827},
|
||||
{37.541207503834414, 67.532770391797811}, {37.54076493934366, 67.532212492318536}};
|
||||
|
||||
auto const bestWay = matcher::GetBestOsmWayOrRelation(osmResponse, geometry);
|
||||
TEST_EQUAL(bestWay.attribute("id").value(), std::string("26232961"), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(ScoreTriangulatedGeometries)
|
||||
{
|
||||
std::vector<m2::PointD> lhs = {{0, 0}, {10, 10}, {10, 0}, {0, 0}, {10, 10}, {0, 10}};
|
||||
|
||||
std::vector<m2::PointD> rhs = {{-1, -1}, {9, 9}, {9, -1}, {-1, -1}, {9, 9}, {-1, 9}};
|
||||
|
||||
auto const score = matcher::ScoreTriangulatedGeometries(lhs, rhs);
|
||||
TEST_GREATER(score, 0.6, ());
|
||||
}
|
||||
} // namespace
|
||||
205
libs/editor/editor_tests/match_by_geometry_test.cpp
Normal file
205
libs/editor/editor_tests/match_by_geometry_test.cpp
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "editor/feature_matcher.hpp"
|
||||
#include "editor/xml_feature.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace
|
||||
{
|
||||
// This place on OSM map https://www.openstreetmap.org/relation/1359233.
|
||||
std::string const kSenatskiyDvorets = R"XXX(
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<osm version="0.6" generator="CGImap 0.6.0 (31854 thorn-01.openstreetmap.org)" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
|
||||
<node id="271895281" visible="true" version="9" changeset="39264478" timestamp="2016-05-12T13:15:43Z" user="Felis Pimeja" uid="260756" lat="55.7578113" lon="37.6220187" />
|
||||
<node id="271895282" visible="true" version="6" changeset="6331881" timestamp="2010-11-09T21:58:31Z" user="Felis Pimeja" uid="260756" lat="55.7587478" lon="37.6223438" />
|
||||
<node id="271895283" visible="true" version="9" changeset="39264478" timestamp="2016-05-12T14:00:50Z" user="Felis Pimeja" uid="260756" lat="55.7590475" lon="37.6221887" />
|
||||
<node id="271895284" visible="true" version="9" changeset="39264478" timestamp="2016-05-12T14:00:50Z" user="Felis Pimeja" uid="260756" lat="55.7587811" lon="37.6206936" />
|
||||
<node id="271895285" visible="true" version="9" changeset="39264478" timestamp="2016-05-12T14:00:50Z" user="Felis Pimeja" uid="260756" lat="55.7585581" lon="37.6208892" />
|
||||
<node id="271895287" visible="true" version="8" changeset="39264478" timestamp="2016-05-12T13:15:44Z" user="Felis Pimeja" uid="260756" lat="55.7579401" lon="37.6212370" />
|
||||
<node id="271895288" visible="true" version="8" changeset="39264478" timestamp="2016-05-12T13:15:44Z" user="Felis Pimeja" uid="260756" lat="55.7580549" lon="37.6218815" />
|
||||
<node id="353142031" visible="true" version="5" changeset="39264478" timestamp="2016-05-12T13:11:39Z" user="Felis Pimeja" uid="260756" lat="55.7582873" lon="37.6213727" />
|
||||
<node id="353142032" visible="true" version="5" changeset="39264478" timestamp="2016-05-12T13:11:40Z" user="Felis Pimeja" uid="260756" lat="55.7581121" lon="37.6214459" />
|
||||
<node id="353142033" visible="true" version="5" changeset="39264478" timestamp="2016-05-12T13:11:40Z" user="Felis Pimeja" uid="260756" lat="55.7583325" lon="37.6216261" />
|
||||
<node id="353142034" visible="true" version="5" changeset="39264478" timestamp="2016-05-12T13:11:40Z" user="Felis Pimeja" uid="260756" lat="55.7581614" lon="37.6217225" />
|
||||
<node id="353142039" visible="true" version="7" changeset="39264478" timestamp="2016-05-12T14:00:50Z" user="Felis Pimeja" uid="260756" lat="55.7585468" lon="37.6208256" />
|
||||
<node id="353142040" visible="true" version="4" changeset="6331881" timestamp="2010-11-09T21:48:55Z" user="Felis Pimeja" uid="260756" lat="55.7587047" lon="37.6217831" />
|
||||
<node id="353142041" visible="true" version="4" changeset="6331881" timestamp="2010-11-09T22:01:20Z" user="Felis Pimeja" uid="260756" lat="55.7585349" lon="37.6218703" />
|
||||
<node id="353142042" visible="true" version="5" changeset="39264478" timestamp="2016-05-12T14:03:22Z" user="Felis Pimeja" uid="260756" lat="55.7585608" lon="37.6220405" />
|
||||
<node id="353142043" visible="true" version="4" changeset="6331881" timestamp="2010-11-09T21:58:14Z" user="Felis Pimeja" uid="260756" lat="55.7587140" lon="37.6220934" />
|
||||
<node id="353142044" visible="true" version="4" changeset="6331881" timestamp="2010-11-09T21:51:38Z" user="Felis Pimeja" uid="260756" lat="55.7587923" lon="37.6220481" />
|
||||
<node id="353142045" visible="true" version="4" changeset="6331881" timestamp="2010-11-09T21:49:59Z" user="Felis Pimeja" uid="260756" lat="55.7587555" lon="37.6218406" />
|
||||
<node id="353142046" visible="true" version="4" changeset="39264478" timestamp="2016-05-12T14:03:22Z" user="Felis Pimeja" uid="260756" lat="55.7587009" lon="37.6218191" />
|
||||
<node id="353142049" visible="true" version="5" changeset="39264478" timestamp="2016-05-12T13:11:40Z" user="Felis Pimeja" uid="260756" lat="55.7582228" lon="37.6220672" />
|
||||
<node id="353142050" visible="true" version="3" changeset="6331881" timestamp="2010-11-09T21:48:58Z" user="Felis Pimeja" uid="260756" lat="55.7582714" lon="37.6220589" />
|
||||
<node id="353142051" visible="true" version="4" changeset="6331881" timestamp="2010-11-09T22:01:30Z" user="Felis Pimeja" uid="260756" lat="55.7584225" lon="37.6221015" />
|
||||
<node id="671182909" visible="true" version="4" changeset="39264478" timestamp="2016-05-12T13:15:44Z" user="Felis Pimeja" uid="260756" lat="55.7578298" lon="37.6221226" />
|
||||
<node id="983727885" visible="true" version="2" changeset="39264478" timestamp="2016-05-12T13:11:46Z" user="Felis Pimeja" uid="260756" lat="55.7584302" lon="37.6220342" />
|
||||
<node id="983728327" visible="true" version="2" changeset="39264478" timestamp="2016-05-12T14:03:22Z" user="Felis Pimeja" uid="260756" lat="55.7587356" lon="37.6218592" />
|
||||
<node id="983728709" visible="true" version="2" changeset="39264478" timestamp="2016-05-12T13:11:46Z" user="Felis Pimeja" uid="260756" lat="55.7582566" lon="37.6213646" />
|
||||
<node id="983730214" visible="true" version="2" changeset="39264478" timestamp="2016-05-12T13:11:46Z" user="Felis Pimeja" uid="260756" lat="55.7582607" lon="37.6213877" />
|
||||
<node id="983730647" visible="true" version="1" changeset="6331881" timestamp="2010-11-09T21:43:48Z" user="Felis Pimeja" uid="260756" lat="55.7582481" lon="37.6220778" />
|
||||
<node id="983731126" visible="true" version="2" changeset="39264478" timestamp="2016-05-12T14:03:22Z" user="Felis Pimeja" uid="260756" lat="55.7587086" lon="37.6220650" />
|
||||
<node id="983731305" visible="true" version="2" changeset="39264478" timestamp="2016-05-12T13:11:46Z" user="Felis Pimeja" uid="260756" lat="55.7583984" lon="37.6218559" />
|
||||
<node id="983732911" visible="true" version="2" changeset="39264478" timestamp="2016-05-12T13:11:47Z" user="Felis Pimeja" uid="260756" lat="55.7582046" lon="37.6219650" />
|
||||
<node id="4181167714" visible="true" version="2" changeset="39264478" timestamp="2016-05-12T13:15:44Z" user="Felis Pimeja" uid="260756" lat="55.7580204" lon="37.6216881">
|
||||
<tag k="entrance" v="yes" />
|
||||
</node>
|
||||
<node id="4181259382" visible="true" version="1" changeset="39264478" timestamp="2016-05-12T14:03:16Z" user="Felis Pimeja" uid="260756" lat="55.7588440" lon="37.6222940" />
|
||||
<node id="4420391892" visible="true" version="1" changeset="42470341" timestamp="2016-09-27T13:17:17Z" user="literan" uid="830106" lat="55.7588469" lon="37.6210627">
|
||||
<tag k="entrance" v="main" />
|
||||
</node>
|
||||
<way id="25010101" visible="true" version="12" changeset="42470341" timestamp="2016-09-27T13:17:18Z" user="literan" uid="830106">
|
||||
<nd ref="271895283" />
|
||||
<nd ref="4420391892" />
|
||||
<nd ref="271895284" />
|
||||
<nd ref="353142039" />
|
||||
<nd ref="271895285" />
|
||||
<nd ref="271895287" />
|
||||
<nd ref="4181167714" />
|
||||
<nd ref="271895288" />
|
||||
<nd ref="271895281" />
|
||||
<nd ref="671182909" />
|
||||
</way>
|
||||
<way id="31560451" visible="true" version="2" changeset="6331881" timestamp="2010-11-09T22:01:05Z" user="Felis Pimeja" uid="260756">
|
||||
<nd ref="353142031" />
|
||||
<nd ref="353142033" />
|
||||
<nd ref="353142034" />
|
||||
<nd ref="353142032" />
|
||||
<nd ref="983728709" />
|
||||
<nd ref="983730214" />
|
||||
<nd ref="353142031" />
|
||||
</way>
|
||||
<way id="31560453" visible="true" version="2" changeset="6331881" timestamp="2010-11-09T22:00:32Z" user="Felis Pimeja" uid="260756">
|
||||
<nd ref="353142040" />
|
||||
<nd ref="353142041" />
|
||||
<nd ref="353142042" />
|
||||
<nd ref="983731126" />
|
||||
<nd ref="353142043" />
|
||||
<nd ref="353142044" />
|
||||
<nd ref="353142045" />
|
||||
<nd ref="983728327" />
|
||||
<nd ref="353142046" />
|
||||
<nd ref="353142040" />
|
||||
</way>
|
||||
<way id="31560454" visible="true" version="3" changeset="39264478" timestamp="2016-05-12T13:11:32Z" user="Felis Pimeja" uid="260756">
|
||||
<nd ref="353142049" />
|
||||
<nd ref="983730647" />
|
||||
<nd ref="353142050" />
|
||||
<nd ref="353142051" />
|
||||
<nd ref="983727885" />
|
||||
</way>
|
||||
<way id="417596299" visible="true" version="2" changeset="39264478" timestamp="2016-05-12T14:03:19Z" user="Felis Pimeja" uid="260756">
|
||||
<nd ref="671182909" />
|
||||
<nd ref="271895282" />
|
||||
<nd ref="4181259382" />
|
||||
<nd ref="271895283" />
|
||||
</way>
|
||||
<way id="417596307" visible="true" version="1" changeset="39264478" timestamp="2016-05-12T13:11:18Z" user="Felis Pimeja" uid="260756">
|
||||
<nd ref="983727885" />
|
||||
<nd ref="983731305" />
|
||||
<nd ref="983732911" />
|
||||
<nd ref="353142049" />
|
||||
</way>
|
||||
<relation id="85761" visible="true" version="15" changeset="44993517" timestamp="2017-01-08T04:43:37Z" user="Alexander-II" uid="3580412">
|
||||
<member type="way" ref="25010101" role="outer" />
|
||||
<member type="way" ref="417596299" role="outer" />
|
||||
<member type="way" ref="31560451" role="inner" />
|
||||
<member type="way" ref="31560453" role="inner" />
|
||||
<member type="way" ref="417596307" role="inner" />
|
||||
<member type="way" ref="31560454" role="inner" />
|
||||
<tag k="addr:city" v="Москва" />
|
||||
<tag k="addr:country" v="RU" />
|
||||
<tag k="addr:housenumber" v="2" />
|
||||
<tag k="addr:street" v="Театральный проезд" />
|
||||
<tag k="building" v="yes" />
|
||||
<tag k="building:colour" v="tan" />
|
||||
<tag k="building:levels" v="5" />
|
||||
<tag k="contact:phone" v="+7 499 5017800" />
|
||||
<tag k="contact:website" v="http://metmos.ru" />
|
||||
<tag k="int_name" v="Hotel Metropol" />
|
||||
<tag k="name" v="Метрополь" />
|
||||
<tag k="name:de" v="Hotel Metropol" />
|
||||
<tag k="name:el" v="Ξενοδοχείο Μετροπόλ" />
|
||||
<tag k="name:en" v="Hotel Metropol" />
|
||||
<tag k="name:pl" v="Hotel Metropol" />
|
||||
<tag k="opening_hours" v="24/7" />
|
||||
<tag k="roof:material" v="metal" />
|
||||
<tag k="tourism" v="hotel" />
|
||||
<tag k="type" v="multipolygon" />
|
||||
<tag k="wikidata" v="Q2034313" />
|
||||
<tag k="wikipedia" v="ru:Метрополь (гостиница, Москва)" />
|
||||
</relation>
|
||||
</osm>
|
||||
)XXX";
|
||||
|
||||
UNIT_TEST(MatchByGeometry)
|
||||
{
|
||||
pugi::xml_document osmResponse;
|
||||
TEST(osmResponse.load_buffer(kSenatskiyDvorets.c_str(), kSenatskiyDvorets.size()), ());
|
||||
|
||||
// It is a triangulated polygon. Every triangle is presented as three points.
|
||||
// For simplification, you can visualize it as a single sequence of points
|
||||
// by using, for ex. Gnuplot.
|
||||
std::vector<m2::PointD> geometry = {
|
||||
{37.621818614168603, 67.468231078000599}, {37.621858847304139, 67.468292768808396},
|
||||
{37.621783745451154, 67.468236442418657}, {37.621783745451154, 67.468236442418657},
|
||||
{37.621858847304139, 67.468292768808396}, {37.621840071840893, 67.468327637525846},
|
||||
{37.621783745451154, 67.468236442418657}, {37.621840071840893, 67.468327637525846},
|
||||
{37.620694768582979, 67.46837323507944}, {37.621783745451154, 67.468236442418657},
|
||||
{37.620694768582979, 67.46837323507944}, {37.620887887633501, 67.46797626814228},
|
||||
{37.620887887633501, 67.46797626814228}, {37.620694768582979, 67.46837323507944},
|
||||
{37.620826196825703, 67.467957492679034}, {37.621783745451154, 67.468236442418657},
|
||||
{37.620887887633501, 67.46797626814228}, {37.621625495118082, 67.467576618996077},
|
||||
{37.621625495118082, 67.467576618996077}, {37.620887887633501, 67.46797626814228},
|
||||
{37.621373367468806, 67.467496152725033}, {37.621373367468806, 67.467496152725033},
|
||||
{37.620887887633501, 67.46797626814228}, {37.621365320841704, 67.467439826335323},
|
||||
{37.621373367468806, 67.467496152725033}, {37.621365320841704, 67.467439826335323},
|
||||
{37.621386778513994, 67.467447872962424}, {37.621783745451154, 67.468236442418657},
|
||||
{37.621625495118082, 67.467576618996077}, {37.621869576140256, 67.467936035006772},
|
||||
{37.621869576140256, 67.467936035006772}, {37.621625495118082, 67.467576618996077},
|
||||
{37.621856165095096, 67.467691953984598}, {37.621856165095096, 67.467691953984598},
|
||||
{37.621625495118082, 67.467576618996077}, {37.621966135665531, 67.467348631228134},
|
||||
{37.621966135665531, 67.467348631228134}, {37.621625495118082, 67.467576618996077},
|
||||
{37.621722054643357, 67.467270847166105}, {37.621966135665531, 67.467348631228134},
|
||||
{37.621722054643357, 67.467270847166105}, {37.621880304976401, 67.46708309253367},
|
||||
{37.621880304976401, 67.46708309253367}, {37.621722054643357, 67.467270847166105},
|
||||
{37.621445787112748, 67.467185016477004}, {37.621880304976401, 67.46708309253367},
|
||||
{37.621445787112748, 67.467185016477004}, {37.621236574808023, 67.466879244647032},
|
||||
{37.621236574808023, 67.466879244647032}, {37.621445787112748, 67.467185016477004},
|
||||
{37.621365320841704, 67.467439826335323}, {37.621236574808023, 67.466879244647032},
|
||||
{37.621365320841704, 67.467439826335323}, {37.620887887633501, 67.46797626814228},
|
||||
{37.621966135665531, 67.467348631228134}, {37.621880304976401, 67.46708309253367},
|
||||
{37.622068059608836, 67.46738081773654}, {37.622068059608836, 67.46738081773654},
|
||||
{37.621880304976401, 67.46708309253367}, {37.622121703789531, 67.466683443387467},
|
||||
{37.622121703789531, 67.466683443387467}, {37.621880304976401, 67.46708309253367},
|
||||
{37.622019779846227, 67.466648574670018}, {37.622068059608836, 67.46738081773654},
|
||||
{37.622121703789531, 67.466683443387467}, {37.622078788444981, 67.467426415290134},
|
||||
{37.621869576140256, 67.467936035006772}, {37.621856165095096, 67.467691953984598},
|
||||
{37.622033190891386, 67.467748280374309}, {37.621869576140256, 67.467936035006772},
|
||||
{37.622033190891386, 67.467748280374309}, {37.622041237518488, 67.467981632560367},
|
||||
{37.622041237518488, 67.467981632560367}, {37.622033190891386, 67.467748280374309},
|
||||
{37.62210024611727, 67.467734869329149}, {37.622041237518488, 67.467981632560367},
|
||||
{37.62210024611727, 67.467734869329149}, {37.622344327139444, 67.468314226480686},
|
||||
{37.622344327139444, 67.468314226480686}, {37.62210024611727, 67.467734869329149},
|
||||
{37.622078788444981, 67.467426415290134}, {37.622078788444981, 67.467426415290134},
|
||||
{37.62210024611727, 67.467734869329149}, {37.622060012981734, 67.46746664842567},
|
||||
{37.622344327139444, 67.468314226480686}, {37.622078788444981, 67.467426415290134},
|
||||
{37.622121703789531, 67.466683443387467}, {37.622041237518488, 67.467981632560367},
|
||||
{37.622344327139444, 67.468314226480686}, {37.622092199490169, 67.468252535672889},
|
||||
{37.622092199490169, 67.468252535672889}, {37.622344327139444, 67.468314226480686},
|
||||
{37.622049284145589, 67.468392010542686}, {37.622049284145589, 67.468392010542686},
|
||||
{37.622344327139444, 67.468314226480686}, {37.622188759015415, 67.468845303869585},
|
||||
{37.622049284145589, 67.468392010542686}, {37.622188759015415, 67.468845303869585},
|
||||
{37.621840071840893, 67.468327637525846}, {37.621840071840893, 67.468327637525846},
|
||||
{37.622188759015415, 67.468845303869585}, {37.620694768582979, 67.46837323507944},
|
||||
{37.622041237518488, 67.467981632560367}, {37.622092199490169, 67.468252535672889},
|
||||
{37.622065377399821, 67.468244489045759}};
|
||||
|
||||
auto const matched = matcher::GetBestOsmWayOrRelation(osmResponse, geometry);
|
||||
TEST_EQUAL(matched.attribute("id").value(), std::string("85761"), ());
|
||||
}
|
||||
} // namespace
|
||||
38
libs/editor/editor_tests/new_feature_categories_test.cpp
Normal file
38
libs/editor/editor_tests/new_feature_categories_test.cpp
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "editor/editor_config.hpp"
|
||||
#include "editor/new_feature_categories.hpp"
|
||||
|
||||
#include "indexer/classificator_loader.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
UNIT_TEST(NewFeatureCategories_UniqueNames)
|
||||
{
|
||||
classificator::Load();
|
||||
|
||||
editor::EditorConfig config;
|
||||
osm::NewFeatureCategories categories(config);
|
||||
|
||||
for (auto const & locale : CategoriesHolder::kLocaleMapping)
|
||||
{
|
||||
std::string const lang(locale.m_name);
|
||||
categories.AddLanguage(lang);
|
||||
auto names = categories.GetAllCreatableTypeNames();
|
||||
std::sort(names.begin(), names.end());
|
||||
auto result = std::unique(names.begin(), names.end());
|
||||
|
||||
if (result != names.end())
|
||||
{
|
||||
LOG(LWARNING, ("Types duplication detected! The following types are duplicated:"));
|
||||
do
|
||||
{
|
||||
LOG(LWARNING, (*result));
|
||||
}
|
||||
while (++result != names.end());
|
||||
|
||||
TEST(false, ("Please look at output above"));
|
||||
}
|
||||
}
|
||||
}
|
||||
267
libs/editor/editor_tests/opening_hours_ui_test.cpp
Normal file
267
libs/editor/editor_tests/opening_hours_ui_test.cpp
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "editor/opening_hours_ui.hpp"
|
||||
|
||||
#include <set>
|
||||
|
||||
using namespace editor::ui;
|
||||
|
||||
UNIT_TEST(TestTimeTable)
|
||||
{
|
||||
{
|
||||
TimeTable tt = TimeTable::GetUninitializedTimeTable();
|
||||
TEST(!tt.IsValid(), ());
|
||||
}
|
||||
{
|
||||
auto tt = TimeTable::GetPredefinedTimeTable();
|
||||
TEST(tt.IsValid(), ());
|
||||
TEST(tt.IsTwentyFourHours(), ());
|
||||
|
||||
TEST(tt.RemoveWorkingDay(osmoh::Weekday::Sunday), ());
|
||||
TEST(tt.RemoveWorkingDay(osmoh::Weekday::Monday), ());
|
||||
TEST(tt.RemoveWorkingDay(osmoh::Weekday::Tuesday), ());
|
||||
TEST(tt.RemoveWorkingDay(osmoh::Weekday::Wednesday), ());
|
||||
TEST(tt.RemoveWorkingDay(osmoh::Weekday::Thursday), ());
|
||||
TEST(tt.RemoveWorkingDay(osmoh::Weekday::Friday), ());
|
||||
TEST(!tt.RemoveWorkingDay(osmoh::Weekday::Saturday), ());
|
||||
|
||||
TEST_EQUAL(tt.GetOpeningDays(), (std::set<osmoh::Weekday>{osmoh::Weekday::Saturday}), ());
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(TestTimeTable_ExcludeTime)
|
||||
{
|
||||
using osmoh::operator""_h;
|
||||
using osmoh::operator""_min;
|
||||
using osmoh::HourMinutes;
|
||||
|
||||
{
|
||||
auto tt = TimeTable::GetPredefinedTimeTable();
|
||||
tt.SetTwentyFourHours(false);
|
||||
|
||||
tt.SetOpeningTime({8_h + 15_min, 18_h + 30_min});
|
||||
TEST(tt.AddExcludeTime({10_h, 11_h}), ());
|
||||
TEST(tt.AddExcludeTime({12_h, 13_h}), ());
|
||||
TEST(tt.AddExcludeTime({15_h, 17_h}), ());
|
||||
TEST_EQUAL(tt.GetExcludeTime().size(), 3, ());
|
||||
|
||||
TEST(tt.SetOpeningTime({8_h + 15_min, 12_h + 30_min}), ());
|
||||
TEST_EQUAL(tt.GetExcludeTime().size(), 1, ());
|
||||
}
|
||||
{
|
||||
auto tt = TimeTable::GetPredefinedTimeTable();
|
||||
tt.SetTwentyFourHours(false);
|
||||
|
||||
tt.SetOpeningTime({8_h + 15_min, 18_h + 30_min});
|
||||
TEST(tt.AddExcludeTime({10_h, 12_h}), ());
|
||||
TEST(tt.AddExcludeTime({11_h, 13_h}), ());
|
||||
TEST(tt.AddExcludeTime({15_h, 17_h}), ());
|
||||
TEST_EQUAL(tt.GetExcludeTime().size(), 2, ());
|
||||
}
|
||||
{
|
||||
auto tt = TimeTable::GetPredefinedTimeTable();
|
||||
tt.SetTwentyFourHours(false);
|
||||
|
||||
tt.SetOpeningTime({8_h + 15_min, 18_h + 30_min});
|
||||
TEST(tt.AddExcludeTime({10_h, 17_h}), ());
|
||||
TEST(tt.AddExcludeTime({11_h, 13_h}), ());
|
||||
TEST(tt.AddExcludeTime({15_h, 17_h}), ());
|
||||
TEST_EQUAL(tt.GetExcludeTime().size(), 1, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetStart().GetHourMinutes().GetHoursCount(), 10, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetEnd().GetHourMinutes().GetHoursCount(), 17, ());
|
||||
}
|
||||
{
|
||||
auto tt = TimeTable::GetPredefinedTimeTable();
|
||||
tt.SetTwentyFourHours(false);
|
||||
|
||||
tt.SetOpeningTime({11_h + 15_min, 18_h + 30_min});
|
||||
TEST(!tt.AddExcludeTime({10_h, 12_h}), ());
|
||||
TEST(!tt.AddExcludeTime({11_h, 13_h}), ());
|
||||
TEST(tt.AddExcludeTime({15_h, 17_h}), ());
|
||||
TEST_EQUAL(tt.GetExcludeTime().size(), 1, ());
|
||||
}
|
||||
{
|
||||
auto tt = TimeTable::GetPredefinedTimeTable();
|
||||
tt.SetTwentyFourHours(false);
|
||||
|
||||
tt.SetOpeningTime({8_h + 15_min, 2_h + 30_min});
|
||||
TEST(tt.AddExcludeTime({10_h, 15_h}), ());
|
||||
TEST(tt.AddExcludeTime({16_h, 2_h}), ());
|
||||
TEST(tt.AddExcludeTime({16_h, 22_h}), ());
|
||||
TEST_EQUAL(tt.GetExcludeTime().size(), 2, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetStart().GetHourMinutes().GetHoursCount(), 10, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetEnd().GetHourMinutes().GetHoursCount(), 15, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[1].GetStart().GetHourMinutes().GetHoursCount(), 16, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[1].GetEnd().GetHourMinutes().GetHoursCount(), 2, ());
|
||||
}
|
||||
{
|
||||
auto tt = TimeTable::GetPredefinedTimeTable();
|
||||
tt.SetTwentyFourHours(false);
|
||||
|
||||
tt.SetOpeningTime({8_h + 15_min, 15_h + 30_min});
|
||||
TEST(!tt.AddExcludeTime({10_h, 16_h}), ());
|
||||
TEST(!tt.AddExcludeTime({7_h, 14_h}), ());
|
||||
}
|
||||
{
|
||||
auto tt = TimeTable::GetPredefinedTimeTable();
|
||||
tt.SetTwentyFourHours(false);
|
||||
|
||||
tt.SetOpeningTime({8_h + 15_min, 18_h + 30_min});
|
||||
TEST(tt.AddExcludeTime({10_h, 11_h}), ());
|
||||
TEST(tt.AddExcludeTime({12_h, 13_h}), ());
|
||||
TEST(tt.AddExcludeTime({15_h, 17_h}), ());
|
||||
TEST(tt.ReplaceExcludeTime({13_h, 14_h}, 1), ());
|
||||
TEST(tt.ReplaceExcludeTime({10_h + 30_min, 14_h}, 1), ());
|
||||
TEST_EQUAL(tt.GetExcludeTime().size(), 2, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetStart().GetHourMinutes().GetHoursCount(), 10, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetEnd().GetHourMinutes().GetHoursCount(), 14, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[1].GetStart().GetHourMinutes().GetHoursCount(), 15, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[1].GetEnd().GetHourMinutes().GetHoursCount(), 17, ());
|
||||
}
|
||||
{
|
||||
auto tt = TimeTable::GetPredefinedTimeTable();
|
||||
tt.SetTwentyFourHours(false);
|
||||
|
||||
tt.SetOpeningTime({8_h + 15_min, 23_h + 30_min});
|
||||
TEST(tt.AddExcludeTime({10_h, 11_h}), ());
|
||||
TEST(tt.AddExcludeTime({12_h, 13_h}), ());
|
||||
TEST(tt.AddExcludeTime({15_h, 17_h}), ());
|
||||
TEST_EQUAL(tt.GetPredefinedExcludeTime().GetStart().GetHourMinutes().GetHoursCount(), 20, ());
|
||||
TEST_EQUAL(tt.GetPredefinedExcludeTime().GetStart().GetHourMinutes().GetMinutesCount(), 0, ());
|
||||
TEST_EQUAL(tt.GetPredefinedExcludeTime().GetEnd().GetHourMinutes().GetHoursCount(), 21, ());
|
||||
TEST_EQUAL(tt.GetPredefinedExcludeTime().GetEnd().GetHourMinutes().GetMinutesCount(), 0, ());
|
||||
|
||||
TEST(tt.AddExcludeTime({18_h, 23_h}), ());
|
||||
auto const predefinedStart = tt.GetPredefinedExcludeTime().GetStart().GetHourMinutes();
|
||||
auto const predefinedEnd = tt.GetPredefinedExcludeTime().GetEnd().GetHourMinutes();
|
||||
TEST(predefinedStart.GetHours() == HourMinutes::THours::zero(), ());
|
||||
TEST(predefinedStart.GetMinutes() == HourMinutes::TMinutes::zero(), ());
|
||||
TEST(predefinedEnd.GetHours() == HourMinutes::THours::zero(), ());
|
||||
TEST(predefinedEnd.GetMinutes() == HourMinutes::TMinutes::zero(), ());
|
||||
}
|
||||
{
|
||||
auto tt = TimeTable::GetPredefinedTimeTable();
|
||||
tt.SetTwentyFourHours(false);
|
||||
|
||||
tt.SetOpeningTime({8_h, 7_h});
|
||||
auto const predefinedStart = tt.GetPredefinedExcludeTime().GetStart().GetHourMinutes();
|
||||
auto const predefinedEnd = tt.GetPredefinedExcludeTime().GetEnd().GetHourMinutes();
|
||||
TEST(predefinedStart.GetHours() == HourMinutes::THours::zero(), ());
|
||||
TEST(predefinedStart.GetMinutes() == HourMinutes::TMinutes::zero(), ());
|
||||
TEST(predefinedEnd.GetHours() == HourMinutes::THours::zero(), ());
|
||||
TEST(predefinedEnd.GetMinutes() == HourMinutes::TMinutes::zero(), ());
|
||||
}
|
||||
{
|
||||
auto tt = TimeTable::GetPredefinedTimeTable();
|
||||
tt.SetTwentyFourHours(false);
|
||||
|
||||
tt.SetOpeningTime({7_h, 8_h + 45_min});
|
||||
TEST(!tt.CanAddExcludeTime(), ());
|
||||
}
|
||||
{
|
||||
auto tt = TimeTable::GetPredefinedTimeTable();
|
||||
tt.SetTwentyFourHours(false);
|
||||
|
||||
tt.SetOpeningTime({19_h, 18_h});
|
||||
auto const predefinedStart = tt.GetPredefinedExcludeTime().GetStart().GetHourMinutes();
|
||||
auto const predefinedEnd = tt.GetPredefinedExcludeTime().GetEnd().GetHourMinutes();
|
||||
TEST(predefinedStart.GetHours() == HourMinutes::THours::zero(), ());
|
||||
TEST(predefinedStart.GetMinutes() == HourMinutes::TMinutes::zero(), ());
|
||||
TEST(predefinedEnd.GetHours() == HourMinutes::THours::zero(), ());
|
||||
TEST(predefinedEnd.GetMinutes() == HourMinutes::TMinutes::zero(), ());
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(TestAppendTimeTable)
|
||||
{
|
||||
{
|
||||
TimeTableSet tts;
|
||||
TEST(!tts.Empty(), ());
|
||||
|
||||
{
|
||||
auto tt = tts.Back();
|
||||
|
||||
TEST(tt.RemoveWorkingDay(osmoh::Weekday::Sunday), ());
|
||||
TEST(tt.RemoveWorkingDay(osmoh::Weekday::Saturday), ());
|
||||
TEST(tt.Commit(), ());
|
||||
|
||||
TEST(tts.Append(tts.GetComplementTimeTable()), ());
|
||||
TEST_EQUAL(tts.Back().GetOpeningDays(),
|
||||
(std::set<osmoh::Weekday>{osmoh::Weekday::Sunday, osmoh::Weekday::Saturday}), ());
|
||||
}
|
||||
|
||||
{
|
||||
auto tt = tts.Front();
|
||||
TEST(tt.RemoveWorkingDay(osmoh::Weekday::Monday), ());
|
||||
TEST(tt.RemoveWorkingDay(osmoh::Weekday::Tuesday), ());
|
||||
TEST(tt.Commit(), ());
|
||||
}
|
||||
|
||||
TEST(tts.Append(tts.GetComplementTimeTable()), ());
|
||||
TEST_EQUAL(tts.Back().GetOpeningDays(), (std::set<osmoh::Weekday>{osmoh::Weekday::Monday, osmoh::Weekday::Tuesday}),
|
||||
());
|
||||
|
||||
TEST(!tts.GetComplementTimeTable().IsValid(), ());
|
||||
TEST(!tts.Append(tts.GetComplementTimeTable()), ());
|
||||
TEST_EQUAL(tts.Size(), 3, ());
|
||||
|
||||
TEST(tts.Remove(0), ());
|
||||
TEST(tts.Remove(1), ());
|
||||
TEST_EQUAL(tts.Size(), 1, ());
|
||||
TEST_EQUAL(tts.GetUnhandledDays(),
|
||||
(std::set<osmoh::Weekday>{osmoh::Weekday::Monday, osmoh::Weekday::Tuesday, osmoh::Weekday::Wednesday,
|
||||
osmoh::Weekday::Thursday, osmoh::Weekday::Friday}),
|
||||
());
|
||||
}
|
||||
{
|
||||
TimeTableSet tts;
|
||||
auto tt = tts.GetComplementTimeTable();
|
||||
tt.AddWorkingDay(osmoh::Weekday::Friday);
|
||||
tt.AddWorkingDay(osmoh::Weekday::Saturday);
|
||||
tt.AddWorkingDay(osmoh::Weekday::Sunday);
|
||||
|
||||
TEST(tts.Append(tt), ());
|
||||
|
||||
TEST_EQUAL(tts.Size(), 2, ());
|
||||
TEST_EQUAL(tts.Front().GetOpeningDays().size(), 4, ());
|
||||
TEST_EQUAL(tts.Back().GetOpeningDays().size(), 3, ());
|
||||
|
||||
TEST(!tts.GetComplementTimeTable().IsValid(), ());
|
||||
}
|
||||
{
|
||||
TimeTableSet tts;
|
||||
auto tt = tts.GetComplementTimeTable();
|
||||
tt.AddWorkingDay(osmoh::Weekday::Friday);
|
||||
|
||||
TEST(tts.Append(tt), ());
|
||||
|
||||
TEST_EQUAL(tts.Size(), 2, ());
|
||||
TEST_EQUAL(tts.Front().GetOpeningDays().size(), 6, ());
|
||||
TEST_EQUAL(tts.Back().GetOpeningDays().size(), 1, ());
|
||||
|
||||
TEST(!tts.GetComplementTimeTable().IsValid(), ());
|
||||
|
||||
tt = tts.Front();
|
||||
tt.AddWorkingDay(osmoh::Weekday::Friday);
|
||||
TEST(!tts.Append(tt), ());
|
||||
TEST_EQUAL(tts.Front().GetOpeningDays().size(), 6, ());
|
||||
TEST_EQUAL(tts.Back().GetOpeningDays().size(), 1, ());
|
||||
}
|
||||
{
|
||||
TimeTableSet tts;
|
||||
|
||||
{
|
||||
auto tt = tts.GetComplementTimeTable();
|
||||
tt.AddWorkingDay(osmoh::Weekday::Friday);
|
||||
TEST(tts.Append(tt), ());
|
||||
}
|
||||
|
||||
TEST_EQUAL(tts.Size(), 2, ());
|
||||
TEST_EQUAL(tts.Front().GetOpeningDays().size(), 6, ());
|
||||
TEST_EQUAL(tts.Back().GetOpeningDays().size(), 1, ());
|
||||
|
||||
auto tt = tts.Front();
|
||||
tt.AddWorkingDay(osmoh::Weekday::Friday);
|
||||
TEST(!tt.Commit(), ());
|
||||
}
|
||||
}
|
||||
1399
libs/editor/editor_tests/osm_editor_test.cpp
Normal file
1399
libs/editor/editor_tests/osm_editor_test.cpp
Normal file
File diff suppressed because it is too large
Load diff
92
libs/editor/editor_tests/osm_editor_test.hpp
Normal file
92
libs/editor/editor_tests/osm_editor_test.hpp
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#pragma once
|
||||
|
||||
#include "generator/generator_tests_support/test_mwm_builder.hpp"
|
||||
|
||||
#include "editor/editable_data_source.hpp"
|
||||
|
||||
#include "indexer/mwm_set.hpp"
|
||||
|
||||
#include "storage/country_info_getter.hpp"
|
||||
|
||||
#include "platform/local_country_file_utils.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace editor
|
||||
{
|
||||
namespace testing
|
||||
{
|
||||
class EditorTest
|
||||
{
|
||||
public:
|
||||
EditorTest();
|
||||
~EditorTest();
|
||||
|
||||
void GetFeatureTypeInfoTest();
|
||||
void GetEditedFeatureTest();
|
||||
void SetIndexTest();
|
||||
void GetEditedFeatureStreetTest();
|
||||
void GetFeatureStatusTest();
|
||||
void IsFeatureUploadedTest();
|
||||
void DeleteFeatureTest();
|
||||
void ClearAllLocalEditsTest();
|
||||
void GetFeaturesByStatusTest();
|
||||
void OnMapDeregisteredTest();
|
||||
void RollBackChangesTest();
|
||||
void HaveMapEditsOrNotesToUploadTest();
|
||||
void HaveMapEditsToUploadTest();
|
||||
void GetStatsTest();
|
||||
void IsCreatedFeatureTest();
|
||||
void ForEachFeatureInMwmRectAndScaleTest();
|
||||
void CreateNoteTest();
|
||||
void LoadMapEditsTest();
|
||||
void SaveEditedFeatureTest();
|
||||
void SaveTransactionTest();
|
||||
void LoadExistingEditsXml();
|
||||
|
||||
private:
|
||||
template <typename BuildFn>
|
||||
MwmSet::MwmId ConstructTestMwm(BuildFn && fn)
|
||||
{
|
||||
return BuildMwm("TestCountry", std::forward<BuildFn>(fn));
|
||||
}
|
||||
|
||||
template <typename BuildFn>
|
||||
MwmSet::MwmId BuildMwm(std::string const & name, BuildFn && fn, int64_t version = 0)
|
||||
{
|
||||
m_mwmFiles.emplace_back(GetPlatform().WritableDir(), platform::CountryFile(name), version);
|
||||
auto & file = m_mwmFiles.back();
|
||||
Cleanup(file);
|
||||
|
||||
{
|
||||
generator::tests_support::TestMwmBuilder builder(file, feature::DataHeader::MapType::Country);
|
||||
fn(builder);
|
||||
}
|
||||
|
||||
auto result = m_dataSource.RegisterMap(file);
|
||||
CHECK_EQUAL(result.second, MwmSet::RegResult::Success, ());
|
||||
|
||||
auto const & id = result.first;
|
||||
|
||||
auto const & info = id.GetInfo();
|
||||
if (info)
|
||||
m_infoGetter.AddCountry(storage::CountryDef(name, info->m_bordersRect));
|
||||
|
||||
CHECK(id.IsAlive(), ());
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
void Cleanup(platform::LocalCountryFile const & map);
|
||||
bool RemoveMwm(MwmSet::MwmId const & mwmId);
|
||||
|
||||
EditableDataSource m_dataSource;
|
||||
storage::CountryInfoGetterForTesting m_infoGetter;
|
||||
std::vector<platform::LocalCountryFile> m_mwmFiles;
|
||||
};
|
||||
} // namespace testing
|
||||
} // namespace editor
|
||||
538
libs/editor/editor_tests/ui2oh_test.cpp
Normal file
538
libs/editor/editor_tests/ui2oh_test.cpp
Normal file
|
|
@ -0,0 +1,538 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "editor/ui2oh.hpp"
|
||||
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
using namespace osmoh;
|
||||
using namespace editor;
|
||||
using namespace editor::ui;
|
||||
|
||||
UNIT_TEST(OpeningHours2TimeTableSet)
|
||||
{
|
||||
{
|
||||
OpeningHours oh("08:00-22:00");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 1, ());
|
||||
|
||||
auto const tt = tts.Front();
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 7, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 8, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 22, ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Su 11:00-23:00;");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 1, ());
|
||||
|
||||
auto const tt = tts.Front();
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 7, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 11, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 23, ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh(
|
||||
"Mo-Su 12:00-15:30, 19:30-23:00;"
|
||||
"Fr-Sa 12:00-15:30, 19:30-23:30;");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 2, ());
|
||||
{
|
||||
auto const tt = tts.Front();
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 5, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 12, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 23, ());
|
||||
|
||||
TEST_EQUAL(tt.GetExcludeTime().size(), 1, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetStart().GetHourMinutes().GetHoursCount(), 15, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetStart().GetHourMinutes().GetMinutesCount(), 30, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetEnd().GetHourMinutes().GetHoursCount(), 19, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetEnd().GetHourMinutes().GetMinutesCount(), 30, ());
|
||||
}
|
||||
{
|
||||
auto const tt = tts.Back();
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 2, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 12, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 23, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetMinutesCount(), 30, ());
|
||||
|
||||
TEST_EQUAL(tt.GetExcludeTime().size(), 1, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetStart().GetHourMinutes().GetHoursCount(), 15, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetStart().GetHourMinutes().GetMinutesCount(), 30, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetEnd().GetHourMinutes().GetHoursCount(), 19, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetEnd().GetHourMinutes().GetMinutesCount(), 30, ());
|
||||
}
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Fr 08:00-22:00");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 1, ());
|
||||
|
||||
auto const tt = tts.Front();
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 5, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 8, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 22, ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Fr 08:00-12:00, 13:00-22:00");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 1, ());
|
||||
|
||||
auto const tt = tts.Front();
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 5, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 8, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 22, ());
|
||||
|
||||
TEST_EQUAL(tt.GetExcludeTime().size(), 1, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetStart().GetHourMinutes().GetHoursCount(), 12, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetEnd().GetHourMinutes().GetHoursCount(), 13, ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Fr 08:00-10:00, 11:00-12:30, 13:00-22:00");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 1, ());
|
||||
|
||||
auto const tt = tts.Front();
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 5, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 8, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 22, ());
|
||||
|
||||
TEST_EQUAL(tt.GetExcludeTime().size(), 2, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetStart().GetHourMinutes().GetHoursCount(), 10, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetEnd().GetHourMinutes().GetHoursCount(), 11, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[1].GetStart().GetHourMinutes().GetHoursCount(), 12, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[1].GetEnd().GetHourMinutes().GetHoursCount(), 13, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[1].GetStart().GetHourMinutes().GetMinutesCount(), 30, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[1].GetEnd().GetHourMinutes().GetMinutesCount(), 0, ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Fr 08:00-10:00; Su, Sa 13:00-22:00");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 2, ());
|
||||
|
||||
{
|
||||
auto const tt = tts.Get(0);
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 5, ());
|
||||
}
|
||||
|
||||
{
|
||||
auto const tt = tts.Get(1);
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 2, ());
|
||||
}
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Jan Mo-Fr 08:00-10:00");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(!MakeTimeTableSet(oh, tts), ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("2016 Mo-Fr 08:00-10:00");
|
||||
TEST(!oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(!MakeTimeTableSet(oh, tts), ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("week 30 Mo-Fr 08:00-10:00");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(!MakeTimeTableSet(oh, tts), ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Su 11:00-24:00");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 1, ());
|
||||
|
||||
auto const tt = tts.Front();
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 7, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 11, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 24, ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Fr 08:00-10:00; Su, Sa 13:00-22:00");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 2, ());
|
||||
|
||||
{
|
||||
auto const tt = tts.Get(0);
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 5, ());
|
||||
}
|
||||
|
||||
{
|
||||
auto const tt = tts.Get(1);
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 2, ());
|
||||
}
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Fr 08:00-13:00,14:00-20:00; Sa 09:00-13:00,14:00-18:00");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 2, ());
|
||||
|
||||
{
|
||||
auto const tt = tts.Get(0);
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 5, ());
|
||||
}
|
||||
|
||||
{
|
||||
auto const tt = tts.Get(1);
|
||||
TEST(!tt.IsTwentyFourHours(), ());
|
||||
TEST_EQUAL(tt.GetOpeningDays().size(), 1, ());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(OpeningHours2TimeTableSet_off)
|
||||
{
|
||||
{
|
||||
OpeningHours oh("Mo-Fr 08:00-13:00,14:00-20:00; Su off");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Su 08:00-13:00,14:00-20:00; Sa off");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.GetUnhandledDays(), OpeningDays({osmoh::Weekday::Saturday}), ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Sa; Su; Sa off");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.GetUnhandledDays(),
|
||||
OpeningDays({osmoh::Weekday::Monday, osmoh::Weekday::Tuesday, osmoh::Weekday::Wednesday,
|
||||
osmoh::Weekday::Thursday, osmoh::Weekday::Friday, osmoh::Weekday::Saturday}),
|
||||
());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Su 08:00-13:00,14:00-20:00; Sa 10:00-11:00 off");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 2, ());
|
||||
|
||||
auto const tt = tts.Get(1);
|
||||
TEST_EQUAL(tt.GetOpeningDays(), OpeningDays({osmoh::Weekday::Saturday}), ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 8, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 20, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetStart().GetHourMinutes().GetHoursCount(), 10, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetEnd().GetHourMinutes().GetHoursCount(), 11, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[1].GetStart().GetHourMinutes().GetHoursCount(), 13, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[1].GetEnd().GetHourMinutes().GetHoursCount(), 14, ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Su; Sa 10:00-11:00 off");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 2, ());
|
||||
|
||||
auto const tt = tts.Get(1);
|
||||
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 0, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 24, ());
|
||||
|
||||
TEST_EQUAL(tt.GetOpeningDays(), OpeningDays({osmoh::Weekday::Saturday}), ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetStart().GetHourMinutes().GetHoursCount(), 10, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetEnd().GetHourMinutes().GetHoursCount(), 11, ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Fr 11:00-17:00; Sa-Su 12:00-16:00; Tu off");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 2, ());
|
||||
TEST_EQUAL(tts.GetUnhandledDays(), OpeningDays({osmoh::Weekday::Tuesday}), ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Fr 11:00-17:00; Sa-Su 12:00-16:00; Mo-Fr off");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 1, ());
|
||||
|
||||
TEST_EQUAL(tts.GetUnhandledDays(),
|
||||
OpeningDays({osmoh::Weekday::Monday, osmoh::Weekday::Tuesday, osmoh::Weekday::Wednesday,
|
||||
osmoh::Weekday::Thursday, osmoh::Weekday::Friday}),
|
||||
());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo-Fr 11:00-17:00; Sa-Su 12:00-16:00; Mo-Fr 11:00-13:00 off");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 2, ());
|
||||
|
||||
auto const tt = tts.Get(0);
|
||||
TEST_EQUAL(tts.GetUnhandledDays(), OpeningDays(), ());
|
||||
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 11, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 17, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetStart().GetHourMinutes().GetHoursCount(), 11, ());
|
||||
TEST_EQUAL(tt.GetExcludeTime()[0].GetEnd().GetHourMinutes().GetHoursCount(), 13, ());
|
||||
}
|
||||
{
|
||||
OpeningHours oh("Mo off; Tu-Su 09:00-17:00");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 1, ());
|
||||
|
||||
TEST_EQUAL(tts.GetUnhandledDays(), OpeningDays({osmoh::Weekday::Monday}), ());
|
||||
|
||||
auto const tt = tts.Get(0);
|
||||
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 9, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 17, ());
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(OpeningHours2TimeTableSet_plus)
|
||||
{
|
||||
OpeningHours oh("Mo-Su 11:00+");
|
||||
TEST(oh.IsValid(), ());
|
||||
|
||||
TimeTableSet tts;
|
||||
|
||||
TEST(MakeTimeTableSet(oh, tts), ());
|
||||
TEST_EQUAL(tts.Size(), 1, ());
|
||||
|
||||
auto const tt = tts.Get(0);
|
||||
TEST_EQUAL(tts.GetUnhandledDays(), OpeningDays(), ());
|
||||
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetStart().GetHourMinutes().GetHoursCount(), 11, ());
|
||||
TEST_EQUAL(tt.GetOpeningTime().GetEnd().GetHourMinutes().GetHoursCount(), 24, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(TimeTableSt2OpeningHours)
|
||||
{
|
||||
{
|
||||
TimeTableSet tts;
|
||||
TEST_EQUAL(ToString(MakeOpeningHours(tts)), "24/7", ());
|
||||
}
|
||||
{
|
||||
TimeTableSet tts;
|
||||
auto tt = tts.Front();
|
||||
TEST(tt.SetOpeningDays({osmoh::Weekday::Monday, osmoh::Weekday::Tuesday, osmoh::Weekday::Wednesday,
|
||||
osmoh::Weekday::Thursday, osmoh::Weekday::Friday, osmoh::Weekday::Saturday,
|
||||
osmoh::Weekday::Sunday}),
|
||||
());
|
||||
|
||||
tt.SetTwentyFourHours(false);
|
||||
TEST(tt.SetOpeningTime({8_h, 22_h}), ());
|
||||
TEST(tt.Commit(), ());
|
||||
|
||||
TEST_EQUAL(ToString(MakeOpeningHours(tts)), "Mo-Su 08:00-22:00", ());
|
||||
}
|
||||
{
|
||||
TimeTableSet tts;
|
||||
auto tt = tts.Front();
|
||||
TEST(tt.SetOpeningDays({osmoh::Weekday::Monday, osmoh::Weekday::Tuesday, osmoh::Weekday::Wednesday,
|
||||
osmoh::Weekday::Thursday, osmoh::Weekday::Friday}),
|
||||
());
|
||||
|
||||
tt.SetTwentyFourHours(false);
|
||||
TEST(tt.SetOpeningTime({8_h, 22_h}), ());
|
||||
TEST(tt.Commit(), ());
|
||||
|
||||
TEST_EQUAL(ToString(MakeOpeningHours(tts)), "Mo-Fr 08:00-22:00", ());
|
||||
}
|
||||
{
|
||||
TimeTableSet tts;
|
||||
|
||||
auto tt = tts.Front();
|
||||
TEST(tt.SetOpeningDays({osmoh::Weekday::Monday, osmoh::Weekday::Tuesday, osmoh::Weekday::Wednesday,
|
||||
osmoh::Weekday::Thursday, osmoh::Weekday::Friday}),
|
||||
());
|
||||
|
||||
tt.SetTwentyFourHours(false);
|
||||
TEST(tt.SetOpeningTime({8_h, 22_h}), ());
|
||||
TEST(tt.AddExcludeTime({12_h, 13_h}), ());
|
||||
TEST(tt.Commit(), ());
|
||||
|
||||
TEST_EQUAL(ToString(MakeOpeningHours(tts)), "Mo-Fr 08:00-12:00, 13:00-22:00", ());
|
||||
}
|
||||
{
|
||||
TimeTableSet tts;
|
||||
|
||||
auto tt = tts.Front();
|
||||
TEST(tt.SetOpeningDays({osmoh::Weekday::Monday, osmoh::Weekday::Tuesday, osmoh::Weekday::Wednesday,
|
||||
osmoh::Weekday::Thursday, osmoh::Weekday::Friday}),
|
||||
());
|
||||
|
||||
tt.SetTwentyFourHours(false);
|
||||
TEST(tt.SetOpeningTime({8_h, 22_h}), ());
|
||||
TEST(tt.AddExcludeTime({10_h, 11_h}), ());
|
||||
TEST(tt.AddExcludeTime({12_h + 30_min, 13_h}), ());
|
||||
TEST(tt.Commit(), ());
|
||||
|
||||
TEST_EQUAL(ToString(MakeOpeningHours(tts)), "Mo-Fr 08:00-10:00, 11:00-12:30, 13:00-22:00", ());
|
||||
}
|
||||
{
|
||||
TimeTableSet tts;
|
||||
|
||||
{
|
||||
auto tt = tts.Front();
|
||||
TEST(tt.SetOpeningDays({osmoh::Weekday::Tuesday, osmoh::Weekday::Wednesday, osmoh::Weekday::Thursday}), ());
|
||||
|
||||
tt.SetTwentyFourHours(false);
|
||||
TEST(tt.SetOpeningTime({8_h, 10_h}), ());
|
||||
TEST(tt.Commit(), ());
|
||||
}
|
||||
{
|
||||
TimeTable tt = TimeTable::GetUninitializedTimeTable();
|
||||
TEST(tt.SetOpeningDays(
|
||||
{osmoh::Weekday::Monday, osmoh::Weekday::Friday, osmoh::Weekday::Saturday, osmoh::Weekday::Sunday}),
|
||||
());
|
||||
|
||||
tt.SetTwentyFourHours(false);
|
||||
TEST(tt.SetOpeningTime({13_h, 22_h}), ());
|
||||
TEST(tts.Append(tt), ());
|
||||
}
|
||||
|
||||
TEST_EQUAL(ToString(MakeOpeningHours(tts)), "Tu-Th 08:00-10:00; Fr-Mo 13:00-22:00", ());
|
||||
}
|
||||
{
|
||||
TimeTableSet tts;
|
||||
|
||||
{
|
||||
auto tt = tts.Front();
|
||||
TEST(tt.SetOpeningDays({osmoh::Weekday::Monday, osmoh::Weekday::Wednesday, osmoh::Weekday::Friday}), ());
|
||||
|
||||
tt.SetTwentyFourHours(false);
|
||||
TEST(tt.SetOpeningTime({8_h, 10_h}), ());
|
||||
TEST(tt.Commit(), ());
|
||||
}
|
||||
{
|
||||
TimeTable tt = TimeTable::GetUninitializedTimeTable();
|
||||
TEST(tt.SetOpeningDays({osmoh::Weekday::Saturday, osmoh::Weekday::Sunday}), ());
|
||||
|
||||
tt.SetTwentyFourHours(false);
|
||||
TEST(tt.SetOpeningTime({13_h, 22_h}), ());
|
||||
TEST(tts.Append(tt), ());
|
||||
}
|
||||
|
||||
TEST_EQUAL(ToString(MakeOpeningHours(tts)), "Mo, We, Fr 08:00-10:00; Sa-Su 13:00-22:00", ());
|
||||
}
|
||||
{
|
||||
TimeTableSet tts;
|
||||
|
||||
auto tt = tts.Front();
|
||||
TEST(tt.SetOpeningDays({osmoh::Weekday::Sunday, osmoh::Weekday::Monday, osmoh::Weekday::Tuesday,
|
||||
osmoh::Weekday::Wednesday, osmoh::Weekday::Thursday, osmoh::Weekday::Friday,
|
||||
osmoh::Weekday::Saturday}),
|
||||
());
|
||||
|
||||
tt.SetTwentyFourHours(false);
|
||||
TEST(tt.SetOpeningTime({11_h, 24_h}), ());
|
||||
TEST(tt.Commit(), ());
|
||||
|
||||
TEST_EQUAL(ToString(MakeOpeningHours(tts)), "Mo-Su 11:00-24:00", ());
|
||||
}
|
||||
{
|
||||
TimeTableSet tts;
|
||||
|
||||
{
|
||||
auto tt = tts.Front();
|
||||
TEST(tt.SetOpeningDays({osmoh::Weekday::Monday, osmoh::Weekday::Wednesday, osmoh::Weekday::Thursday}), ());
|
||||
|
||||
tt.SetTwentyFourHours(false);
|
||||
TEST(tt.SetOpeningTime({8_h, 20_h}), ());
|
||||
TEST(tt.AddExcludeTime({13_h, 14_h}), ());
|
||||
TEST(tt.Commit(), ());
|
||||
}
|
||||
{
|
||||
TimeTable tt = TimeTable::GetUninitializedTimeTable();
|
||||
TEST(tt.SetOpeningDays({osmoh::Weekday::Saturday}), ());
|
||||
|
||||
tt.SetTwentyFourHours(false);
|
||||
TEST(tt.SetOpeningTime({9_h, 18_h}), ());
|
||||
TEST(tt.AddExcludeTime({13_h, 14_h}), ());
|
||||
TEST(tts.Append(tt), ());
|
||||
}
|
||||
|
||||
TEST_EQUAL(ToString(MakeOpeningHours(tts)),
|
||||
"Mo, We-Th 08:00-13:00, 14:00-20:00; "
|
||||
"Sa 09:00-13:00, 14:00-18:00",
|
||||
());
|
||||
}
|
||||
}
|
||||
531
libs/editor/editor_tests/xml_feature_test.cpp
Normal file
531
libs/editor/editor_tests/xml_feature_test.cpp
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "editor/xml_feature.hpp"
|
||||
|
||||
#include "indexer/classificator_loader.hpp"
|
||||
#include "indexer/editable_map_object.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "base/timer.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
using namespace editor;
|
||||
|
||||
UNIT_TEST(XMLFeature_RawGetSet)
|
||||
{
|
||||
XMLFeature feature(XMLFeature::Type::Node);
|
||||
TEST(!feature.HasTag("opening_hours"), ());
|
||||
TEST(!feature.HasAttribute("center"), ());
|
||||
|
||||
feature.SetAttribute("FooBar", "foobar");
|
||||
TEST_EQUAL(feature.GetAttribute("FooBar"), "foobar", ());
|
||||
|
||||
feature.SetAttribute("FooBar", "foofoo");
|
||||
TEST_EQUAL(feature.GetAttribute("FooBar"), "foofoo", ());
|
||||
|
||||
feature.SetTagValue("opening_hours", "18:20-18:21");
|
||||
TEST_EQUAL(feature.GetTagValue("opening_hours"), "18:20-18:21", ());
|
||||
|
||||
feature.SetTagValue("opening_hours", "18:20-19:21");
|
||||
TEST_EQUAL(feature.GetTagValue("opening_hours"), "18:20-19:21", ());
|
||||
|
||||
auto const expected = R"(<?xml version="1.0"?>
|
||||
<node FooBar="foofoo">
|
||||
<tag k="opening_hours" v="18:20-19:21" />
|
||||
</node>
|
||||
)";
|
||||
|
||||
std::stringstream sstr;
|
||||
feature.Save(sstr);
|
||||
TEST_EQUAL(expected, sstr.str(), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_Setters)
|
||||
{
|
||||
XMLFeature feature(XMLFeature::Type::Node);
|
||||
|
||||
feature.SetCenter(mercator::FromLatLon(55.7978998, 37.4745280));
|
||||
feature.SetModificationTime(base::StringToTimestamp("2015-11-27T21:13:32Z"));
|
||||
|
||||
feature.SetName("Gorki Park");
|
||||
feature.SetName("en", "Gorki Park");
|
||||
feature.SetName("ru", "Парк Горького");
|
||||
feature.SetName("int_name", "Gorky Park");
|
||||
|
||||
feature.SetHouse("10");
|
||||
feature.SetTagValue("opening_hours", "Mo-Fr 08:15-17:30");
|
||||
feature.SetTagValue("amenity", "atm");
|
||||
|
||||
std::stringstream sstr;
|
||||
feature.Save(sstr);
|
||||
|
||||
auto const expectedString = R"(<?xml version="1.0"?>
|
||||
<node lat="55.7978998" lon="37.474528" timestamp="2015-11-27T21:13:32Z">
|
||||
<tag k="name" v="Gorki Park" />
|
||||
<tag k="name:en" v="Gorki Park" />
|
||||
<tag k="name:ru" v="Парк Горького" />
|
||||
<tag k="int_name" v="Gorky Park" />
|
||||
<tag k="addr:housenumber" v="10" />
|
||||
<tag k="opening_hours" v="Mo-Fr 08:15-17:30" />
|
||||
<tag k="amenity" v="atm" />
|
||||
</node>
|
||||
)";
|
||||
|
||||
TEST_EQUAL(sstr.str(), expectedString, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_UintLang)
|
||||
{
|
||||
XMLFeature feature(XMLFeature::Type::Node);
|
||||
|
||||
feature.SetCenter(mercator::FromLatLon(55.79, 37.47));
|
||||
feature.SetModificationTime(base::StringToTimestamp("2015-11-27T21:13:32Z"));
|
||||
|
||||
feature.SetName(StringUtf8Multilang::kDefaultCode, "Gorki Park");
|
||||
feature.SetName(StringUtf8Multilang::GetLangIndex("ru"), "Парк Горького");
|
||||
feature.SetName(StringUtf8Multilang::kInternationalCode, "Gorky Park");
|
||||
std::stringstream sstr;
|
||||
feature.Save(sstr);
|
||||
|
||||
auto const expectedString = R"(<?xml version="1.0"?>
|
||||
<node lat="55.79" lon="37.47" timestamp="2015-11-27T21:13:32Z">
|
||||
<tag k="name" v="Gorki Park" />
|
||||
<tag k="name:ru" v="Парк Горького" />
|
||||
<tag k="int_name" v="Gorky Park" />
|
||||
</node>
|
||||
)";
|
||||
|
||||
TEST_EQUAL(sstr.str(), expectedString, ());
|
||||
|
||||
XMLFeature f2(expectedString);
|
||||
TEST_EQUAL(f2.GetName(StringUtf8Multilang::kDefaultCode), "Gorki Park", ());
|
||||
TEST_EQUAL(f2.GetName(StringUtf8Multilang::GetLangIndex("ru")), "Парк Горького", ());
|
||||
TEST_EQUAL(f2.GetName(StringUtf8Multilang::kInternationalCode), "Gorky Park", ());
|
||||
|
||||
TEST_EQUAL(f2.GetName(), "Gorki Park", ());
|
||||
TEST_EQUAL(f2.GetName("default"), "Gorki Park", ());
|
||||
TEST_EQUAL(f2.GetName("ru"), "Парк Горького", ());
|
||||
TEST_EQUAL(f2.GetName("int_name"), "Gorky Park", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_ToOSMString)
|
||||
{
|
||||
XMLFeature feature(XMLFeature::Type::Node);
|
||||
feature.SetCenter(mercator::FromLatLon(55.7978998, 37.4745280));
|
||||
feature.SetName("OSM");
|
||||
feature.SetTagValue("amenity", "atm");
|
||||
|
||||
auto const expectedString = R"(<?xml version="1.0"?>
|
||||
<osm>
|
||||
<node lat="55.7978998" lon="37.474528">
|
||||
<tag k="name" v="OSM" />
|
||||
<tag k="amenity" v="atm" />
|
||||
</node>
|
||||
</osm>
|
||||
)";
|
||||
TEST_EQUAL(expectedString, feature.ToOSMString(), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_HasTags)
|
||||
{
|
||||
auto const taggedNode = R"(
|
||||
<node lat="55.7978998" lon="37.474528" timestamp="2015-11-27T21:13:32Z">
|
||||
<tag k="name" v="OSM" />
|
||||
<tag k="amenity" v="atm" />
|
||||
</node>
|
||||
)";
|
||||
XMLFeature taggedFeature(taggedNode);
|
||||
TEST(taggedFeature.HasAnyTags(), ());
|
||||
TEST(taggedFeature.HasTag("amenity"), ());
|
||||
TEST(taggedFeature.HasKey("amenity"), ());
|
||||
TEST(!taggedFeature.HasTag("name:en"), ());
|
||||
TEST(taggedFeature.HasKey("lon"), ());
|
||||
TEST(!taggedFeature.HasTag("lon"), ());
|
||||
TEST_EQUAL(taggedFeature.GetTagValue("name"), "OSM", ());
|
||||
TEST_EQUAL(taggedFeature.GetTagValue("nope"), "", ());
|
||||
|
||||
constexpr char const * emptyWay = R"(
|
||||
<way timestamp="2015-11-27T21:13:32Z"/>
|
||||
)";
|
||||
XMLFeature emptyFeature(emptyWay);
|
||||
TEST(!emptyFeature.HasAnyTags(), ());
|
||||
TEST(emptyFeature.HasAttribute("timestamp"), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_FromXml)
|
||||
{
|
||||
// Do not space-align this string literal constant. It will be compared below.
|
||||
auto const kTestNode = R"(<?xml version="1.0"?>
|
||||
<node lat="55.7978998" lon="37.474528" timestamp="2015-11-27T21:13:32Z">
|
||||
<tag k="name" v="Gorki Park" />
|
||||
<tag k="name:en" v="Gorki Park" />
|
||||
<tag k="name:ru" v="Парк Горького" />
|
||||
<tag k="int_name" v="Gorky Park" />
|
||||
<tag k="addr:housenumber" v="10" />
|
||||
<tag k="opening_hours" v="Mo-Fr 08:15-17:30" />
|
||||
<tag k="amenity" v="atm" />
|
||||
</node>
|
||||
)";
|
||||
|
||||
std::map<std::string_view, std::string_view> kTestNames{
|
||||
{"default", "Gorki Park"}, {"en", "Gorki Park"}, {"ru", "Парк Горького"}, {"int_name", "Gorky Park"}};
|
||||
|
||||
XMLFeature feature(kTestNode);
|
||||
|
||||
std::stringstream sstr;
|
||||
feature.Save(sstr);
|
||||
TEST_EQUAL(kTestNode, sstr.str(), ());
|
||||
|
||||
TEST(feature.HasKey("opening_hours"), ());
|
||||
TEST(feature.HasKey("lat"), ());
|
||||
TEST(feature.HasKey("lon"), ());
|
||||
TEST(!feature.HasKey("FooBarBaz"), ());
|
||||
|
||||
TEST_EQUAL(feature.GetHouse(), "10", ());
|
||||
TEST_EQUAL(feature.GetCenter(), ms::LatLon(55.7978998, 37.4745280), ());
|
||||
TEST_EQUAL(feature.GetName(), kTestNames["default"], ());
|
||||
TEST_EQUAL(feature.GetName("default"), kTestNames["default"], ());
|
||||
TEST_EQUAL(feature.GetName("en"), kTestNames["en"], ());
|
||||
TEST_EQUAL(feature.GetName("ru"), kTestNames["ru"], ());
|
||||
TEST_EQUAL(feature.GetName("int_name"), kTestNames["int_name"], ());
|
||||
TEST_EQUAL(feature.GetName("No such language"), "", ());
|
||||
|
||||
TEST_EQUAL(feature.GetTagValue("opening_hours"), "Mo-Fr 08:15-17:30", ());
|
||||
TEST_EQUAL(feature.GetTagValue("amenity"), "atm", ());
|
||||
TEST_EQUAL(base::TimestampToString(feature.GetModificationTime()), "2015-11-27T21:13:32Z", ());
|
||||
|
||||
std::map<std::string_view, std::string_view> names;
|
||||
feature.ForEachName([&names](std::string_view lang, std::string_view name) { names.emplace(lang, name); });
|
||||
TEST_EQUAL(names, kTestNames, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_FromOSM)
|
||||
{
|
||||
auto const kTestNodeWay = R"(<?xml version="1.0"?>
|
||||
<osm>
|
||||
<node id="4" lat="55.7978998" lon="37.474528" timestamp="2015-11-27T21:13:32Z">
|
||||
<tag k="test" v="value"/>
|
||||
</node>
|
||||
<node id="5" lat="55.7977777" lon="37.474528" timestamp="2015-11-27T21:13:33Z"/>
|
||||
<way id="3" timestamp="2015-11-27T21:13:34Z">
|
||||
<nd ref="4"/>
|
||||
<nd ref="5"/>
|
||||
<tag k="hi" v="test"/>
|
||||
</way>
|
||||
</osm>
|
||||
)";
|
||||
|
||||
TEST_ANY_THROW(XMLFeature::FromOSM(""), ());
|
||||
TEST_ANY_THROW(XMLFeature::FromOSM("This is not XML"), ());
|
||||
TEST_ANY_THROW(XMLFeature::FromOSM("<?xml version=\"1.0\"?>"), ());
|
||||
TEST_NO_THROW(XMLFeature::FromOSM("<?xml version=\"1.0\"?><osm></osm>"), ());
|
||||
TEST_ANY_THROW(XMLFeature::FromOSM("<?xml version=\"1.0\"?><osm><node lat=\"11.11\"/></osm>"), ());
|
||||
std::vector<XMLFeature> features;
|
||||
TEST_NO_THROW(features = XMLFeature::FromOSM(kTestNodeWay), ());
|
||||
TEST_EQUAL(3, features.size(), ());
|
||||
XMLFeature const & node = features[0];
|
||||
TEST_EQUAL(node.GetAttribute("id"), "4", ());
|
||||
TEST_EQUAL(node.GetTagValue("test"), "value", ());
|
||||
TEST_EQUAL(features[2].GetTagValue("hi"), "test", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_FromXmlNode)
|
||||
{
|
||||
auto const kTestNode = R"(<?xml version="1.0"?>
|
||||
<osm>
|
||||
<node id="4" lat="55.7978998" lon="37.474528" timestamp="2015-11-27T21:13:32Z">
|
||||
<tag k="amenity" v="fountain"/>
|
||||
</node>
|
||||
</osm>
|
||||
)";
|
||||
|
||||
pugi::xml_document doc;
|
||||
doc.load_string(kTestNode);
|
||||
XMLFeature const feature(doc.child("osm").child("node"));
|
||||
TEST_EQUAL(feature.GetAttribute("id"), "4", ());
|
||||
TEST_EQUAL(feature.GetTagValue("amenity"), "fountain", ());
|
||||
XMLFeature const copy(feature);
|
||||
TEST_EQUAL(copy.GetAttribute("id"), "4", ());
|
||||
TEST_EQUAL(copy.GetTagValue("amenity"), "fountain", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_Geometry)
|
||||
{
|
||||
std::vector<m2::PointD> const geometry = {
|
||||
{28.7206411, 3.7182409}, {46.7569003, 47.0774689}, {22.5909217, 41.6994874}, {14.7537008, 17.7788229},
|
||||
{55.1261701, 10.3199476}, {28.6519654, 50.0305930}, {28.7206411, 3.7182409}};
|
||||
|
||||
XMLFeature feature(XMLFeature::Type::Way);
|
||||
feature.SetGeometry(geometry);
|
||||
TEST_EQUAL(feature.GetGeometry(), geometry, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_ApplyPatch)
|
||||
{
|
||||
auto const kOsmFeature = R"(<?xml version="1.0"?>
|
||||
<osm>
|
||||
<node id="1" lat="1" lon="2" timestamp="2015-11-27T21:13:32Z" version="1">
|
||||
<tag k="amenity" v="cafe"/>
|
||||
</node>
|
||||
</osm>
|
||||
)";
|
||||
|
||||
auto const kPatch = R"(<?xml version="1.0"?>
|
||||
<node lat="1" lon="2" timestamp="2015-11-27T21:13:32Z">
|
||||
<tag k="website" v="maps.me"/>
|
||||
</node>
|
||||
)";
|
||||
|
||||
XMLFeature const baseOsmFeature = XMLFeature::FromOSM(kOsmFeature).front();
|
||||
|
||||
{
|
||||
XMLFeature noAnyTags = baseOsmFeature;
|
||||
noAnyTags.ApplyPatch(XMLFeature(kPatch));
|
||||
TEST(noAnyTags.HasKey("website"), ());
|
||||
}
|
||||
|
||||
{
|
||||
XMLFeature hasMainTag = baseOsmFeature;
|
||||
hasMainTag.SetTagValue("website", "mapswith.me");
|
||||
hasMainTag.ApplyPatch(XMLFeature(kPatch));
|
||||
TEST_EQUAL(hasMainTag.GetTagValue("website"), "maps.me", ());
|
||||
size_t tagsCount = 0;
|
||||
hasMainTag.ForEachTag([&tagsCount](std::string const &, std::string const &) { ++tagsCount; });
|
||||
TEST_EQUAL(2, tagsCount, ("website should be replaced, not duplicated."));
|
||||
}
|
||||
|
||||
{
|
||||
XMLFeature hasAltTag = baseOsmFeature;
|
||||
hasAltTag.SetTagValue("contact:website", "mapswith.me");
|
||||
hasAltTag.ApplyPatch(XMLFeature(kPatch));
|
||||
TEST(!hasAltTag.HasTag("website"), ("Existing alt tag should be used."));
|
||||
TEST_EQUAL(hasAltTag.GetTagValue("contact:website"), "maps.me", ());
|
||||
}
|
||||
|
||||
{
|
||||
XMLFeature hasAltTag = baseOsmFeature;
|
||||
hasAltTag.SetTagValue("url", "mapswithme.com");
|
||||
hasAltTag.ApplyPatch(XMLFeature(kPatch));
|
||||
TEST(!hasAltTag.HasTag("website"), ("Existing alt tag should be used."));
|
||||
TEST_EQUAL(hasAltTag.GetTagValue("url"), "maps.me", ());
|
||||
}
|
||||
|
||||
{
|
||||
XMLFeature hasTwoAltTags = baseOsmFeature;
|
||||
hasTwoAltTags.SetTagValue("contact:website", "mapswith.me");
|
||||
hasTwoAltTags.SetTagValue("url", "mapswithme.com");
|
||||
hasTwoAltTags.ApplyPatch(XMLFeature(kPatch));
|
||||
TEST(!hasTwoAltTags.HasTag("website"), ("Existing alt tag should be used."));
|
||||
TEST_EQUAL(hasTwoAltTags.GetTagValue("contact:website"), "maps.me", ());
|
||||
TEST_EQUAL(hasTwoAltTags.GetTagValue("url"), "mapswithme.com", ());
|
||||
}
|
||||
|
||||
{
|
||||
XMLFeature hasMainAndAltTag = baseOsmFeature;
|
||||
hasMainAndAltTag.SetTagValue("website", "osmrulezz.com");
|
||||
hasMainAndAltTag.SetTagValue("url", "mapswithme.com");
|
||||
hasMainAndAltTag.ApplyPatch(XMLFeature(kPatch));
|
||||
TEST_EQUAL(hasMainAndAltTag.GetTagValue("website"), "maps.me", ());
|
||||
TEST_EQUAL(hasMainAndAltTag.GetTagValue("url"), "mapswithme.com", ());
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_FromXMLAndBackToXML)
|
||||
{
|
||||
classificator::Load();
|
||||
|
||||
std::string const xmlNoTypeStr = R"(<?xml version="1.0"?>
|
||||
<node lat="55.7978998" lon="37.474528" timestamp="2015-11-27T21:13:32Z">
|
||||
<tag k="name" v="Gorki Park" />
|
||||
<tag k="name:en" v="Gorki Park" />
|
||||
<tag k="name:ru" v="Парк Горького" />
|
||||
<tag k="addr:housenumber" v="10" />
|
||||
</node>
|
||||
)";
|
||||
|
||||
char const kTimestamp[] = "2015-11-27T21:13:32Z";
|
||||
|
||||
editor::XMLFeature xmlNoType(xmlNoTypeStr);
|
||||
editor::XMLFeature xmlWithType = xmlNoType;
|
||||
xmlWithType.SetTagValue("amenity", "atm");
|
||||
|
||||
osm::EditableMapObject emo;
|
||||
editor::FromXML(xmlWithType, emo);
|
||||
auto fromFtWithType = editor::ToXML(emo, true);
|
||||
fromFtWithType.SetAttribute("timestamp", kTimestamp);
|
||||
TEST_EQUAL(fromFtWithType, xmlWithType, ());
|
||||
|
||||
auto fromFtWithoutType = editor::ToXML(emo, false);
|
||||
fromFtWithoutType.SetAttribute("timestamp", kTimestamp);
|
||||
TEST_EQUAL(fromFtWithoutType, xmlNoType, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_AmenityRecyclingFromAndToXml)
|
||||
{
|
||||
classificator::Load();
|
||||
{
|
||||
std::string const recyclingCentreStr = R"(<?xml version="1.0"?>
|
||||
<node lat="55.8047445" lon="37.5865532" timestamp="2018-07-11T13:24:41Z">
|
||||
<tag k="amenity" v="recycling" />
|
||||
<tag k="recycling_type" v="centre" />
|
||||
</node>
|
||||
)";
|
||||
|
||||
char const kTimestamp[] = "2018-07-11T13:24:41Z";
|
||||
|
||||
editor::XMLFeature xmlFeature(recyclingCentreStr);
|
||||
|
||||
osm::EditableMapObject emo;
|
||||
editor::FromXML(xmlFeature, emo);
|
||||
|
||||
auto const th = emo.GetTypes();
|
||||
TEST_EQUAL(th.Size(), 1, ());
|
||||
TEST_EQUAL(th.front(), classif().GetTypeByPath({"amenity", "recycling", "centre"}), ());
|
||||
|
||||
auto convertedFt = editor::ToXML(emo, true);
|
||||
convertedFt.SetAttribute("timestamp", kTimestamp);
|
||||
TEST_EQUAL(xmlFeature, convertedFt, ());
|
||||
}
|
||||
{
|
||||
std::string const recyclingContainerStr = R"(<?xml version="1.0"?>
|
||||
<node lat="55.8047445" lon="37.5865532" timestamp="2018-07-11T13:24:41Z">
|
||||
<tag k="amenity" v="recycling" />
|
||||
<tag k="recycling_type" v="container" />
|
||||
</node>
|
||||
)";
|
||||
|
||||
char const kTimestamp[] = "2018-07-11T13:24:41Z";
|
||||
|
||||
editor::XMLFeature xmlFeature(recyclingContainerStr);
|
||||
|
||||
osm::EditableMapObject emo;
|
||||
editor::FromXML(xmlFeature, emo);
|
||||
|
||||
auto const th = emo.GetTypes();
|
||||
TEST_EQUAL(th.Size(), 1, ());
|
||||
TEST_EQUAL(th.front(), classif().GetTypeByPath({"amenity", "recycling", "container"}), ());
|
||||
|
||||
auto convertedFt = editor::ToXML(emo, true);
|
||||
convertedFt.SetAttribute("timestamp", kTimestamp);
|
||||
TEST_EQUAL(xmlFeature, convertedFt, ());
|
||||
}
|
||||
/*
|
||||
{
|
||||
std::string const recyclingStr = R"(<?xml version="1.0"?>
|
||||
<node lat="55.8047445" lon="37.5865532" timestamp="2018-07-11T13:24:41Z">
|
||||
<tag k="amenity" v="recycling" />
|
||||
</node>
|
||||
)";
|
||||
|
||||
editor::XMLFeature xmlFeature(recyclingStr);
|
||||
|
||||
osm::EditableMapObject emo;
|
||||
editor::FromXML(xmlFeature, emo);
|
||||
|
||||
auto const th = emo.GetTypes();
|
||||
TEST_EQUAL(th.Size(), 1, ());
|
||||
TEST_EQUAL(*th.begin(), classif().GetTypeByPath({"amenity", "recycling"}), ());
|
||||
|
||||
auto convertedFt = editor::ToXML(emo, true);
|
||||
|
||||
// We save recycling container with "recycling_type"="container" tag.
|
||||
TEST(convertedFt.HasTag("recycling_type"), ());
|
||||
TEST_EQUAL(convertedFt.GetTagValue("recycling_type"), "container", ());
|
||||
|
||||
TEST(convertedFt.HasTag("amenity"), ());
|
||||
TEST_EQUAL(convertedFt.GetTagValue("amenity"), "recycling", ());
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_Diet)
|
||||
{
|
||||
XMLFeature ft(XMLFeature::Type::Node);
|
||||
TEST(ft.GetCuisine().empty(), ());
|
||||
|
||||
ft.SetCuisine("vegan;vegetarian");
|
||||
TEST_EQUAL(ft.GetCuisine(), "vegan;vegetarian", ());
|
||||
|
||||
ft.SetCuisine("vegan;pasta;vegetarian");
|
||||
TEST_EQUAL(ft.GetCuisine(), "pasta;vegan;vegetarian", ());
|
||||
|
||||
ft.SetCuisine("vegetarian");
|
||||
TEST_EQUAL(ft.GetCuisine(), "vegetarian", ());
|
||||
|
||||
ft.SetCuisine("vegan");
|
||||
TEST_EQUAL(ft.GetCuisine(), "vegan", ());
|
||||
|
||||
ft.SetCuisine("");
|
||||
TEST_EQUAL(ft.GetCuisine(), "", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_SocialContactsProcessing)
|
||||
{
|
||||
{
|
||||
std::string const nightclubStr = R"(<?xml version="1.0"?>
|
||||
<node lat="50.4082862" lon="30.5130017" timestamp="2022-02-24T05:07:00Z">
|
||||
<tag k="amenity" v="nightclub" />
|
||||
<tag k="name" v="Stereo Plaza" />
|
||||
<tag k="contact:facebook" v="http://www.facebook.com/pages/Stereo-Plaza/118100041593935" />
|
||||
<tag k="contact:instagram" v="https://www.instagram.com/p/CSy87IhMhfm/" />
|
||||
<tag k="contact:line" v="liff.line.me/1645278921-kWRPP32q/?accountId=673watcr" />
|
||||
</node>
|
||||
)";
|
||||
|
||||
editor::XMLFeature xmlFeature(nightclubStr);
|
||||
|
||||
osm::EditableMapObject emo;
|
||||
editor::FromXML(xmlFeature, emo);
|
||||
|
||||
auto convertedFt = editor::ToXML(emo, true);
|
||||
|
||||
TEST(convertedFt.HasTag("contact:facebook"), ());
|
||||
TEST_EQUAL(convertedFt.GetTagValue("contact:facebook"), "https://facebook.com/pages/Stereo-Plaza/118100041593935",
|
||||
());
|
||||
|
||||
TEST(convertedFt.HasTag("contact:instagram"), ());
|
||||
TEST_EQUAL(convertedFt.GetTagValue("contact:instagram"), "https://instagram.com/p/CSy87IhMhfm", ());
|
||||
|
||||
TEST(convertedFt.HasTag("contact:line"), ());
|
||||
TEST_EQUAL(convertedFt.GetTagValue("contact:line"), "https://liff.line.me/1645278921-kWRPP32q/?accountId=673watcr",
|
||||
());
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(XMLFeature_SocialContactsProcessing_clean)
|
||||
{
|
||||
{
|
||||
std::string const nightclubStr = R"(<?xml version="1.0"?>
|
||||
<node lat="40.82862" lon="20.30017" timestamp="2022-02-24T05:07:00Z">
|
||||
<tag k="amenity" v="bar" />
|
||||
<tag k="name" v="Irish Pub" />
|
||||
<tag k="contact:facebook" v="https://www.facebook.com/PierreCardinPeru.oficial/" />
|
||||
<tag k="contact:instagram" v="https://www.instagram.com/fraback.genusswelt/" />
|
||||
<tag k="contact:line" v="https://line.me/R/ti/p/%40015qevdv" />
|
||||
</node>
|
||||
)";
|
||||
|
||||
editor::XMLFeature xmlFeature(nightclubStr);
|
||||
|
||||
osm::EditableMapObject emo;
|
||||
editor::FromXML(xmlFeature, emo);
|
||||
|
||||
auto convertedFt = editor::ToXML(emo, true);
|
||||
|
||||
TEST(convertedFt.HasTag("contact:facebook"), ());
|
||||
TEST_EQUAL(convertedFt.GetTagValue("contact:facebook"), "PierreCardinPeru.oficial", ());
|
||||
|
||||
TEST(convertedFt.HasTag("contact:instagram"), ());
|
||||
TEST_EQUAL(convertedFt.GetTagValue("contact:instagram"), "fraback.genusswelt", ());
|
||||
|
||||
TEST(convertedFt.HasTag("contact:line"), ());
|
||||
TEST_EQUAL(convertedFt.GetTagValue("contact:line"), "015qevdv", ());
|
||||
}
|
||||
}
|
||||
10
libs/editor/editor_tests_support/CMakeLists.txt
Normal file
10
libs/editor/editor_tests_support/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
project(editor_tests_support)
|
||||
|
||||
set(SRC
|
||||
helpers.cpp
|
||||
helpers.hpp
|
||||
)
|
||||
|
||||
omim_add_library(${PROJECT_NAME} ${SRC})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME} editor)
|
||||
29
libs/editor/editor_tests_support/helpers.cpp
Normal file
29
libs/editor/editor_tests_support/helpers.cpp
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#include "editor/editor_tests_support/helpers.hpp"
|
||||
|
||||
#include "editor/editor_storage.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace editor
|
||||
{
|
||||
namespace tests_support
|
||||
{
|
||||
void SetUpEditorForTesting(std::unique_ptr<osm::Editor::Delegate> delegate)
|
||||
{
|
||||
auto & editor = osm::Editor::Instance();
|
||||
editor.SetDelegate(std::move(delegate));
|
||||
editor.SetStorageForTesting(std::make_unique<editor::InMemoryStorage>());
|
||||
editor.ClearAllLocalEdits();
|
||||
editor.ResetNotes();
|
||||
}
|
||||
|
||||
void TearDownEditorForTesting()
|
||||
{
|
||||
auto & editor = osm::Editor::Instance();
|
||||
editor.ClearAllLocalEdits();
|
||||
editor.ResetNotes();
|
||||
editor.SetDelegate({});
|
||||
editor.SetDefaultStorage();
|
||||
}
|
||||
} // namespace tests_support
|
||||
} // namespace editor
|
||||
32
libs/editor/editor_tests_support/helpers.hpp
Normal file
32
libs/editor/editor_tests_support/helpers.hpp
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include "editor/osm_editor.hpp"
|
||||
|
||||
#include "indexer/editable_map_object.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace editor
|
||||
{
|
||||
namespace tests_support
|
||||
{
|
||||
void SetUpEditorForTesting(std::unique_ptr<osm::Editor::Delegate> delegate);
|
||||
void TearDownEditorForTesting();
|
||||
|
||||
template <typename Fn>
|
||||
void EditFeature(FeatureType & ft, Fn && fn)
|
||||
{
|
||||
auto & editor = osm::Editor::Instance();
|
||||
|
||||
osm::EditableMapObject emo;
|
||||
emo.SetFromFeatureType(ft);
|
||||
emo.SetEditableProperties(editor.GetEditableProperties(ft));
|
||||
|
||||
fn(emo);
|
||||
|
||||
CHECK_EQUAL(editor.SaveEditedFeature(emo), osm::Editor::SaveResult::SavedSuccessfully, ());
|
||||
}
|
||||
} // namespace tests_support
|
||||
} // namespace editor
|
||||
110
libs/editor/edits_migration.cpp
Normal file
110
libs/editor/edits_migration.cpp
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#include "editor/edits_migration.hpp"
|
||||
|
||||
#include "editor/feature_matcher.hpp"
|
||||
|
||||
#include "indexer/feature.hpp"
|
||||
#include "indexer/feature_source.hpp"
|
||||
|
||||
#include "geometry/intersection_score.hpp"
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace editor
|
||||
{
|
||||
FeatureID MigrateNodeFeatureIndex(osm::Editor::ForEachFeaturesNearByFn & forEach, XMLFeature const & xml,
|
||||
FeatureStatus const featureStatus, GenerateIDFn const & generateID)
|
||||
{
|
||||
if (featureStatus == FeatureStatus::Created)
|
||||
return generateID();
|
||||
|
||||
FeatureID fid;
|
||||
auto count = 0;
|
||||
forEach([&fid, &count](FeatureType const & ft)
|
||||
{
|
||||
if (ft.GetGeomType() != feature::GeomType::Point)
|
||||
return;
|
||||
// TODO(mgsergio): Check that ft and xml correspond to the same feature.
|
||||
fid = ft.GetID();
|
||||
++count;
|
||||
}, mercator::FromLatLon(xml.GetCenter()));
|
||||
|
||||
if (count == 0)
|
||||
MYTHROW(MigrationError, ("No pointed features returned."));
|
||||
|
||||
if (count > 1)
|
||||
LOG(LWARNING, (count, "features returned for point", mercator::FromLatLon(xml.GetCenter())));
|
||||
|
||||
return fid;
|
||||
}
|
||||
|
||||
FeatureID MigrateWayOrRelatonFeatureIndex(
|
||||
osm::Editor::ForEachFeaturesNearByFn & forEach, XMLFeature const & xml,
|
||||
FeatureStatus const /* Unused for now (we don't create/delete area features)*/,
|
||||
GenerateIDFn const & /*Unused for the same reason*/)
|
||||
{
|
||||
std::optional<FeatureID> fid;
|
||||
auto bestScore = 0.6; // initial score is used as a threshold.
|
||||
auto geometry = xml.GetGeometry();
|
||||
auto count = 0;
|
||||
|
||||
if (geometry.empty())
|
||||
MYTHROW(MigrationError, ("Feature has invalid geometry", xml));
|
||||
|
||||
// This can be any point on a feature.
|
||||
auto const someFeaturePoint = geometry[0];
|
||||
|
||||
forEach([&fid, &geometry, &count, &bestScore](FeatureType & ft)
|
||||
{
|
||||
if (ft.GetGeomType() != feature::GeomType::Area)
|
||||
return;
|
||||
++count;
|
||||
|
||||
std::vector<m2::PointD> ftGeometry;
|
||||
assign_range(ftGeometry, ft.GetTrianglesAsPoints(FeatureType::BEST_GEOMETRY));
|
||||
|
||||
double score = 0.0;
|
||||
try
|
||||
{
|
||||
score = matcher::ScoreTriangulatedGeometries(geometry, ftGeometry);
|
||||
}
|
||||
catch (geometry::NotAPolygonException & ex)
|
||||
{
|
||||
LOG(LWARNING, (ex.Msg()));
|
||||
// Support migration for old application versions.
|
||||
// TODO(a): To remove it when version 8.0.x will no longer be supported.
|
||||
base::SortUnique(geometry);
|
||||
base::SortUnique(ftGeometry);
|
||||
score = matcher::ScoreTriangulatedGeometriesByPoints(geometry, ftGeometry);
|
||||
}
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
fid = ft.GetID();
|
||||
}
|
||||
}, someFeaturePoint);
|
||||
|
||||
if (count == 0)
|
||||
MYTHROW(MigrationError, ("No ways returned for point", someFeaturePoint));
|
||||
|
||||
if (!fid)
|
||||
MYTHROW(MigrationError, ("None of returned ways suffice. Possibly, the feature has been deleted."));
|
||||
return *fid;
|
||||
}
|
||||
|
||||
FeatureID MigrateFeatureIndex(osm::Editor::ForEachFeaturesNearByFn & forEach, XMLFeature const & xml,
|
||||
FeatureStatus const featureStatus, GenerateIDFn const & generateID)
|
||||
{
|
||||
switch (xml.GetType())
|
||||
{
|
||||
case XMLFeature::Type::Unknown: MYTHROW(MigrationError, ("Migration for XMLFeature::Type::Unknown is not possible"));
|
||||
case XMLFeature::Type::Node: return MigrateNodeFeatureIndex(forEach, xml, featureStatus, generateID);
|
||||
case XMLFeature::Type::Way:
|
||||
case XMLFeature::Type::Relation: return MigrateWayOrRelatonFeatureIndex(forEach, xml, featureStatus, generateID);
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
} // namespace editor
|
||||
23
libs/editor/edits_migration.hpp
Normal file
23
libs/editor/edits_migration.hpp
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include "editor/osm_editor.hpp"
|
||||
#include "editor/xml_feature.hpp"
|
||||
|
||||
#include "indexer/feature_decl.hpp"
|
||||
#include "indexer/feature_source.hpp"
|
||||
|
||||
#include "base/exception.hpp"
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace editor
|
||||
{
|
||||
DECLARE_EXCEPTION(MigrationError, RootException);
|
||||
|
||||
using GenerateIDFn = std::function<FeatureID()>;
|
||||
|
||||
/// Tries to match xml feature with one on a new mwm and returns FeatureID
|
||||
/// of a found feature, throws MigrationError if migration fails.
|
||||
FeatureID MigrateFeatureIndex(osm::Editor::ForEachFeaturesNearByFn & forEach, XMLFeature const & xml,
|
||||
FeatureStatus const featureStatus, GenerateIDFn const & generateID);
|
||||
} // namespace editor
|
||||
296
libs/editor/feature_matcher.cpp
Normal file
296
libs/editor/feature_matcher.cpp
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
#include "editor/feature_matcher.hpp"
|
||||
|
||||
#include "geometry/intersection_score.hpp"
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
#include "base/stl_helpers.hpp"
|
||||
#include "base/stl_iterator.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using editor::XMLFeature;
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace bg = boost::geometry;
|
||||
|
||||
// Use simple xy coordinates because spherical are not supported by boost::geometry algorithms.
|
||||
using PointXY = bg::model::d2::point_xy<double>;
|
||||
using Polygon = bg::model::polygon<PointXY>;
|
||||
using MultiPolygon = bg::model::multi_polygon<Polygon>;
|
||||
using Linestring = bg::model::linestring<PointXY>;
|
||||
using MultiLinestring = bg::model::multi_linestring<Linestring>;
|
||||
using AreaType = bg::default_area_result<Polygon>::type;
|
||||
|
||||
using ForEachRefFn = std::function<void(XMLFeature const & xmlFt)>;
|
||||
using ForEachWayFn = std::function<void(pugi::xml_node const & way, std::string const & role)>;
|
||||
|
||||
double constexpr kPointDiffEps = 1e-5;
|
||||
|
||||
void AddInnerIfNeeded(pugi::xml_document const & osmResponse, pugi::xml_node const & way, Polygon & dest)
|
||||
{
|
||||
if (dest.inners().empty() || dest.inners().back().empty())
|
||||
return;
|
||||
|
||||
auto const refs = way.select_nodes("nd/@ref");
|
||||
if (refs.empty())
|
||||
return;
|
||||
|
||||
std::string const nodeRef = refs[0].attribute().value();
|
||||
auto const node = osmResponse.select_node(("osm/node[@id='" + nodeRef + "']").data()).node();
|
||||
ASSERT(node, ("OSM response have ref", nodeRef, "but have no node with such id.", osmResponse));
|
||||
XMLFeature xmlFt(node);
|
||||
|
||||
auto const & pt = dest.inners().back().back();
|
||||
m2::PointD lastPoint(pt.x(), pt.y());
|
||||
|
||||
if (lastPoint.EqualDxDy(xmlFt.GetMercatorCenter(), kPointDiffEps))
|
||||
return;
|
||||
|
||||
dest.inners().emplace_back();
|
||||
}
|
||||
|
||||
void MakeOuterRing(MultiLinestring & outerLines, Polygon & dest)
|
||||
{
|
||||
bool const needReverse = outerLines.size() > 1 && bg::equals(outerLines[0].front(), outerLines[1].back());
|
||||
|
||||
for (size_t i = 0; i < outerLines.size(); ++i)
|
||||
{
|
||||
if (needReverse)
|
||||
bg::reverse(outerLines[i]);
|
||||
|
||||
bg::append(dest.outer(), outerLines[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns value form (-Inf, 1]. Negative values are used as penalty, positive as score.
|
||||
double ScoreLatLon(XMLFeature const & xmlFt, ms::LatLon const & latLon)
|
||||
{
|
||||
auto const a = mercator::FromLatLon(xmlFt.GetCenter());
|
||||
auto const b = mercator::FromLatLon(latLon);
|
||||
return 1.0 - (a.Length(b) / kPointDiffEps);
|
||||
}
|
||||
|
||||
void ForEachRefInWay(pugi::xml_document const & osmResponse, pugi::xml_node const & way, ForEachRefFn const & fn)
|
||||
{
|
||||
for (auto const & xNodeRef : way.select_nodes("nd/@ref"))
|
||||
{
|
||||
std::string const nodeRef = xNodeRef.attribute().value();
|
||||
auto const node = osmResponse.select_node(("osm/node[@id='" + nodeRef + "']").data()).node();
|
||||
ASSERT(node, ("OSM response have ref", nodeRef, "but have no node with such id.", osmResponse));
|
||||
XMLFeature xmlFt(node);
|
||||
fn(xmlFt);
|
||||
}
|
||||
}
|
||||
|
||||
void ForEachWayInRelation(pugi::xml_document const & osmResponse, pugi::xml_node const & relation,
|
||||
ForEachWayFn const & fn)
|
||||
{
|
||||
auto const nodesSet = relation.select_nodes("member[@type='way']/@ref");
|
||||
for (auto const & xNodeRef : nodesSet)
|
||||
{
|
||||
std::string const wayRef = xNodeRef.attribute().value();
|
||||
auto const xpath = "osm/way[@id='" + wayRef + "']";
|
||||
auto const way = osmResponse.select_node(xpath.c_str()).node();
|
||||
|
||||
auto const rolePath = "member[@ref='" + wayRef + "']/@role";
|
||||
pugi::xpath_node roleNode = relation.select_node(rolePath.c_str());
|
||||
|
||||
// It is possible to have a wayRef that refers to a way not included in a given relation.
|
||||
// We can skip such ways.
|
||||
if (!way)
|
||||
continue;
|
||||
|
||||
// If more than one way is given and there is one with no role specified,
|
||||
// it's an error. We skip this particular way but try to use others anyway.
|
||||
if (!roleNode && nodesSet.size() != 1)
|
||||
continue;
|
||||
|
||||
std::string const role = roleNode ? roleNode.attribute().value() : "outer";
|
||||
fn(way, role);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Geometry>
|
||||
void AppendWay(pugi::xml_document const & osmResponse, pugi::xml_node const & way, Geometry & dest)
|
||||
{
|
||||
ForEachRefInWay(osmResponse, way, [&dest](XMLFeature const & xmlFt)
|
||||
{
|
||||
auto const & p = xmlFt.GetMercatorCenter();
|
||||
bg::append(dest, boost::make_tuple(p.x, p.y));
|
||||
});
|
||||
}
|
||||
|
||||
Polygon GetWaysGeometry(pugi::xml_document const & osmResponse, pugi::xml_node const & way)
|
||||
{
|
||||
Polygon result;
|
||||
AppendWay(osmResponse, way, result);
|
||||
|
||||
bg::correct(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Polygon GetRelationsGeometry(pugi::xml_document const & osmResponse, pugi::xml_node const & relation)
|
||||
{
|
||||
Polygon result;
|
||||
MultiLinestring outerLines;
|
||||
|
||||
auto const fn = [&osmResponse, &result, &outerLines](pugi::xml_node const & way, std::string const & role)
|
||||
{
|
||||
if (role == "outer")
|
||||
{
|
||||
outerLines.emplace_back();
|
||||
AppendWay(osmResponse, way, outerLines.back());
|
||||
}
|
||||
else if (role == "inner")
|
||||
{
|
||||
if (result.inners().empty())
|
||||
result.inners().emplace_back();
|
||||
|
||||
// Support several inner rings.
|
||||
AddInnerIfNeeded(osmResponse, way, result);
|
||||
AppendWay(osmResponse, way, result.inners().back());
|
||||
}
|
||||
};
|
||||
|
||||
ForEachWayInRelation(osmResponse, relation, fn);
|
||||
|
||||
MakeOuterRing(outerLines, result);
|
||||
bg::correct(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Polygon GetWaysOrRelationsGeometry(pugi::xml_document const & osmResponse, pugi::xml_node const & wayOrRelation)
|
||||
{
|
||||
if (strcmp(wayOrRelation.name(), "way") == 0)
|
||||
return GetWaysGeometry(osmResponse, wayOrRelation);
|
||||
return GetRelationsGeometry(osmResponse, wayOrRelation);
|
||||
}
|
||||
|
||||
/// Returns value form [-1, 1]. Negative values are used as penalty, positive as score.
|
||||
/// |osmResponse| - nodes, ways and relations from osm;
|
||||
/// |wayOrRelation| - either way or relation to be compared agains ourGeometry;
|
||||
/// |ourGeometry| - geometry of a FeatureType;
|
||||
double ScoreGeometry(pugi::xml_document const & osmResponse, pugi::xml_node const & wayOrRelation,
|
||||
std::vector<m2::PointD> const & ourGeometry)
|
||||
{
|
||||
ASSERT(!ourGeometry.empty(), ("Our geometry cannot be empty"));
|
||||
|
||||
auto const their = GetWaysOrRelationsGeometry(osmResponse, wayOrRelation);
|
||||
|
||||
if (bg::is_empty(their))
|
||||
return geometry::kPenaltyScore;
|
||||
|
||||
auto const our = geometry::TrianglesToPolygon(ourGeometry);
|
||||
|
||||
if (bg::is_empty(our))
|
||||
return geometry::kPenaltyScore;
|
||||
|
||||
auto const score = geometry::GetIntersectionScore(our, their);
|
||||
|
||||
// If area of the intersection is a half of the object area, penalty score will be returned.
|
||||
if (score <= 0.5)
|
||||
return geometry::kPenaltyScore;
|
||||
|
||||
return score;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace matcher
|
||||
{
|
||||
pugi::xml_node GetBestOsmNode(pugi::xml_document const & osmResponse, ms::LatLon const & latLon)
|
||||
{
|
||||
double bestScore = geometry::kPenaltyScore;
|
||||
pugi::xml_node bestMatchNode;
|
||||
|
||||
for (auto const & xNode : osmResponse.select_nodes("osm/node"))
|
||||
{
|
||||
try
|
||||
{
|
||||
XMLFeature const xmlFeature(xNode.node());
|
||||
|
||||
double const nodeScore = ScoreLatLon(xmlFeature, latLon);
|
||||
if (nodeScore < 0)
|
||||
continue;
|
||||
|
||||
if (bestScore < nodeScore)
|
||||
{
|
||||
bestScore = nodeScore;
|
||||
bestMatchNode = xNode.node();
|
||||
}
|
||||
}
|
||||
catch (editor::NoLatLon const & ex)
|
||||
{
|
||||
LOG(LWARNING, ("No lat/lon attribute in osm response node.", ex.Msg()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mgsergio): Add a properly defined threshold when more fields will be compared.
|
||||
// if (bestScore < kMiniScoreThreshold)
|
||||
// return pugi::xml_node;
|
||||
|
||||
return bestMatchNode;
|
||||
}
|
||||
|
||||
pugi::xml_node GetBestOsmWayOrRelation(pugi::xml_document const & osmResponse, std::vector<m2::PointD> const & geometry)
|
||||
{
|
||||
double bestScore = geometry::kPenaltyScore;
|
||||
pugi::xml_node bestMatchWay;
|
||||
|
||||
auto const xpath = "osm/way|osm/relation[tag[@k='type' and @v='multipolygon']]";
|
||||
for (auto const & xWayOrRelation : osmResponse.select_nodes(xpath))
|
||||
{
|
||||
double const nodeScore = ScoreGeometry(osmResponse, xWayOrRelation.node(), geometry);
|
||||
|
||||
if (nodeScore < 0)
|
||||
continue;
|
||||
|
||||
if (bestScore < nodeScore)
|
||||
{
|
||||
bestScore = nodeScore;
|
||||
bestMatchWay = xWayOrRelation.node();
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatchWay;
|
||||
}
|
||||
|
||||
double ScoreTriangulatedGeometries(std::vector<m2::PointD> const & lhs, std::vector<m2::PointD> const & rhs)
|
||||
{
|
||||
auto const score = geometry::GetIntersectionScoreForTriangulated(lhs, rhs);
|
||||
|
||||
// If area of the intersection is a half of the object area, penalty score will be returned.
|
||||
if (score <= 0.5)
|
||||
return geometry::kPenaltyScore;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
double ScoreTriangulatedGeometriesByPoints(std::vector<m2::PointD> const & lhs, std::vector<m2::PointD> const & rhs)
|
||||
{
|
||||
// The default comparison operator used in sort above (cmp1) and one that is
|
||||
// used in set_itersection (cmp2) are compatible in that sence that
|
||||
// cmp2(a, b) :- cmp1(a, b) and
|
||||
// cmp1(a, b) :- cmp2(a, b) || a almost equal b.
|
||||
// You can think of cmp2 as !(a >= b).
|
||||
// But cmp2 is not transitive:
|
||||
// i.e. !cmp(a, b) && !cmp(b, c) does NOT implies !cmp(a, c),
|
||||
// |a, b| < eps, |b, c| < eps.
|
||||
// This could lead to unexpected results in set_itersection (with greedy implementation),
|
||||
// but we assume such situation is very unlikely.
|
||||
auto const matched = set_intersection(begin(lhs), end(lhs), begin(rhs), end(rhs), CounterIterator(),
|
||||
[](m2::PointD const & p1, m2::PointD const & p2)
|
||||
{
|
||||
return p1 < p2 && !p1.EqualDxDy(p2, mercator::kPointEqualityEps);
|
||||
}).GetCount();
|
||||
|
||||
return static_cast<double>(matched) / static_cast<double>(lhs.size());
|
||||
}
|
||||
} // namespace matcher
|
||||
27
libs/editor/feature_matcher.hpp
Normal file
27
libs/editor/feature_matcher.hpp
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include "editor/xml_feature.hpp"
|
||||
|
||||
#include "geometry/point2d.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace matcher
|
||||
{
|
||||
/// Returns a node from OSM closest to the latLon, or an empty node if none is close enough.
|
||||
pugi::xml_node GetBestOsmNode(pugi::xml_document const & osmResponse, ms::LatLon const & latLon);
|
||||
/// Returns a way from osm with similar geometry or empty node if can't find such way.
|
||||
/// Throws NotAPolygon exception when |geometry| is not convertible to a polygon.
|
||||
pugi::xml_node GetBestOsmWayOrRelation(pugi::xml_document const & osmResponse,
|
||||
std::vector<m2::PointD> const & geometry);
|
||||
/// Returns value form [-1, 1]. Negative values are used as penalty, positive as score.
|
||||
/// |lhs| and |rhs| - triangulated polygons.
|
||||
/// Throws NotAPolygon exception when either lhs or rhs is not convertible to a polygon.
|
||||
double ScoreTriangulatedGeometries(std::vector<m2::PointD> const & lhs, std::vector<m2::PointD> const & rhs);
|
||||
/// Deprecated, use ScoreTriangulatedGeometries instead. In the older versions of the editor, the
|
||||
/// edits.xml file didn't contain the necessary geometry information, so we cannot restore the
|
||||
/// original geometry of a particular feature and thus can't use the new algo that is dependent on
|
||||
/// correct feature geometry to calculate scores.
|
||||
// TODO(a): To remove it when version 8.0.x is no longer supported.
|
||||
double ScoreTriangulatedGeometriesByPoints(std::vector<m2::PointD> const & lhs, std::vector<m2::PointD> const & rhs);
|
||||
} // namespace matcher
|
||||
135
libs/editor/keys_to_remove.hpp
Normal file
135
libs/editor/keys_to_remove.hpp
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
// Keys that should be removed when a place in OSM is replaced, copied from
|
||||
// https://github.com/mnalis/StreetComplete-taginfo-categorize/blob/master/sc_to_remove.txt
|
||||
|
||||
// Changes to the list: don't remove 'wheelchair' and addresses in the 'contact:' style
|
||||
|
||||
inline constexpr std::string_view kKeysToRemove[] = {
|
||||
"shop_?[1-9]?(:.*)?", "craft_?[1-9]?", "amenity_?[1-9]?", "club_?[1-9]?", "old_amenity",
|
||||
"old_shop", "information", "leisure", "office_?[1-9]?", "tourism",
|
||||
// popular shop=* / craft=* subkeys
|
||||
"marketplace", "household", "swimming_pool", "laundry", "golf", "sports", "ice_cream",
|
||||
"scooter", "music", "retail", "yes", "ticket", "newsagent", "lighting", "truck", "car_repair",
|
||||
"car_parts", "video", "fuel", "farm", "car", "tractor", "hgv", "ski", "sculptor",
|
||||
"hearing_aids", "surf", "photo", "boat", "gas", "kitchen", "anime", "builder", "hairdresser",
|
||||
"security", "bakery", "bakehouse", "fishing", "doors", "kiosk", "market", "bathroom", "lamps",
|
||||
"vacant", "insurance(:.*)?", "caravan", "gift", "bicycle", "bicycle_rental", "insulation",
|
||||
"communication", "mall", "model", "empty", "wood", "hunting", "motorcycle", "trailer",
|
||||
"camera", "water", "fireplace", "outdoor", "blacksmith", "electronics", "fan", "piercing",
|
||||
"stationery", "sensory_friendly(:.*)?", "street_vendor", "sells(:.*)?", "safety_equipment",
|
||||
// obsoleted information
|
||||
"(demolished|abandoned|disused)(:(?!bui).+)?", "was:.*", "not:.*", "damage", "created_by",
|
||||
"check_date", "opening_date", "last_checked", "checked_exists:date", "pharmacy_survey",
|
||||
"old_ref", "update", "import_uuid", "review", "fixme:atp",
|
||||
// classifications / links to external databases
|
||||
"fhrs:.*", "old_fhrs:.*", "fvst:.*", "ncat", "nat_ref", "gnis:.*", "winkelnummer",
|
||||
"type:FR:FINESS", "type:FR:APE", "kvl_hro:amenity", "ref:DK:cvr(:.*)?", "certifications?",
|
||||
"transiscope", "opendata:type", "local_ref", "official_ref",
|
||||
// names and identifications
|
||||
"name_?[1-9]?(:.*)?", ".*_name_?[1-9]?(:.*)?", "noname", "branch(:.*)?", "brand(:.*)?",
|
||||
"not:brand(:.*)?", "network(:.*)?", "operator(:.*)?", "operator_type", "ref", "ref:vatin",
|
||||
"designation", "SEP:CLAVEESC", "identifier", "ref:FR:SIRET", "ref:FR:SIREN", "ref:FR:NAF",
|
||||
"(old_)?ref:FR:prix-carburants",
|
||||
// contacts
|
||||
"contact_person", "phone(:.*)?", "phone_?[1-9]?", "emergency:phone", "emergency_telephone_code",
|
||||
"contact:(?!housenumber$|street$|place$|postcode$|city$|country$|pobox$|unit$).*",
|
||||
"mobile", "fax", "facebook", "instagram", "twitter", "youtube", "telegram", "tiktok", "email",
|
||||
"website_?[1-9]?(:.*)?", "app:.*", "ownership",
|
||||
"url", "url:official", "source_ref:url", "owner",
|
||||
// payments
|
||||
"payment(:.*)?", "payment_multi_fee", "currency(:.*)?", "cash_withdrawal(:.*)?", "fee",
|
||||
"charge", "charge_fee", "money_transfer", "donation:compensation", "paypoint",
|
||||
// generic shop/craft attributes
|
||||
"seasonal", "time", "opening_hours(:.*)?", "check_(in|out)", "wifi", "internet",
|
||||
"internet_access(:.*)?", "second_hand", "self_service", "automated", "license:.*",
|
||||
"bulk_purchase", ".*:covid19", "language:.*", "baby_feeding", "description(:.*)?",
|
||||
"description[0-9]", "min_age", "max_age", "supermarket(:.*)?", "social_facility(:.*)?",
|
||||
"functional", "trade", "wholesale", "sale", "smoking(:outside)?", "zero_waste", "origin",
|
||||
"attraction", "strapline", "dog", "showroom", "toilets?(:.*)?", "sanitary_dump_station",
|
||||
"changing_table(:.*)?", "blind", "company(:.*)?", "stroller", "walk-in",
|
||||
"webshop", "operational_status.*", "status", "drive_through", "surveillance(:.*)?",
|
||||
"outdoor_seating", "indoor_seating", "colour", "access_simple", "floor", "product_category",
|
||||
"guide", "source_url", "category", "kids_area", "kids_area:indoor", "resort", "since", "state",
|
||||
"temporary", "self_checkout", "audio_loop", "related_law(:.*)?", "official_status(:.*)?",
|
||||
// food and drink details
|
||||
"bar", "cafe", "coffee", "microroasting", "microbrewery", "brewery", "real_ale", "taproom",
|
||||
"training", "distillery", "drink(:.*)?", "cocktails", "alcohol", "wine([:_].*)?",
|
||||
"happy_hours", "diet:.*", "cuisine", "ethnic", "tasting", "breakfast", "lunch", "organic",
|
||||
"produced_on_site", "restaurant", "food", "pastry", "pastry_shop", "product", "produce",
|
||||
"chocolate", "fair_trade", "butcher", "reservation(:.*)?", "takeaway(:.*)?", "delivery(:.*)?",
|
||||
"caterer", "real_fire", "flour_fortified", "highchair", "fast_food", "pub", "snack",
|
||||
"confectionery", "drinking_water:refill",
|
||||
// related to repair shops/crafts
|
||||
"service(:.*)?", "motorcycle:.*", "repair", ".*:repair", "electronics_repair(:.*)?",
|
||||
"workshop",
|
||||
// shop=hairdresser, shop=clothes
|
||||
"unisex", "male", "female", "gender", "gender_simple", "lgbtq(:.*)?", "gay", "female:signed",
|
||||
"male:signed",
|
||||
// healthcare
|
||||
"healthcare(:.*)?", "healthcare_.*", "health", "health_.*", "speciality", "medical_.*",
|
||||
"emergency_ward", "facility(:.*)?", "activities", "healthcare_facility(:.*)?",
|
||||
"laboratory(:.*)?", "blood(:.*)?", "blood_components", "infection(:.*)?", "disease(:.*)?",
|
||||
"covid19(:.*)?", "COVID_.*", "CovidVaccineCenterId", "coronaquarantine", "hospital(:.*)?",
|
||||
"hospital_type_id", "emergency_room", "sample_collection(:.*)?", "bed_count", "capacity:beds",
|
||||
"part_time_beds", "personnel:count", "staff_count(:.*)?", "admin_staff", "doctors",
|
||||
"doctors_num", "nurses_num", "counselling_type", "testing_centres", "toilets_number",
|
||||
"urgent_care", "vaccination", "clinic", "hospital", "pharmacy", "alternative", "laboratory",
|
||||
"sample_collection", "provided_for(:.*)?", "social_facility_for", "ambulance", "ward",
|
||||
"HSE_(code|hgid|hgroup|region)", "collection_centre", "design", "AUTORIZATIE", "reg_id",
|
||||
"post_addr", "scope", "ESTADO", "NIVSOCIO", "NO", "EMP_EST", "COD_HAB", "CLA_PERS", "CLA_PRES",
|
||||
"snis_code:.*", "hfac_bed", "hfac_type", "nature", "moph_code", "IJSN:.*", "massgis:id",
|
||||
"OGD-Stmk:.*", "paho:.*", "panchayath", "pbf_contract", "pcode", "pe:minsa:.*", "who:.*",
|
||||
"pharmacy:category", "tactile_paving", "HF_(ID|TYPE|N_EN)", "RoadConn", "bin", "hiv(:.*)?",
|
||||
// accommodation & layout
|
||||
"rooms", "stars", "accommodation", "beds", "capacity(:persons)?", "laundry_service",
|
||||
"guest_house",
|
||||
// amenity=place_of_worship
|
||||
"deanery", "subject:(wikidata|wikipedia|wikimedia_commons)", "church", "church:type",
|
||||
// schools
|
||||
"capacity:(pupils|teachers)", "grades", "population:pupils(:.*)?",
|
||||
"school:(FR|gender|trust|type|type_idn|group:type)", "primary",
|
||||
// clubs
|
||||
"animal(_breeding|_training)?", "billiards(:.*)?", "board_game", "sport_1", "sport:boating",
|
||||
"boat:type", "canoe(_rental|:service)?", "kayak(_rental|:service)?",
|
||||
"sailboat(_rental|:service)?", "horse_riding", "rugby", "boules", "callsign", "card_games",
|
||||
"car_service", "catastro:ref", "chess(:.*)?", "children", "climbing(:.*)?", "club(:.*)?",
|
||||
"communication(:amateur_radio.*)", "community_centre:for", "dffr:network", "dormitory",
|
||||
"education_for:ages", "electrified", "esperanto", "events_venue", "family", "federation",
|
||||
"free_flying(:.*)?", "freemasonry(:.*)?", "free_refill", "gaelic_games(:.*)?", "membership",
|
||||
"military_service", "model_aerodrome(:.*)?", "mode_of_organisation(:.*)?", "snowmobile",
|
||||
"social_centre(:for)?", "source_dat", "tennis", "old_website", "organisation", "school_type",
|
||||
"scout(:type)?", "fraternity", "live_music", "lockable", "playground(:theme)?", "nudism",
|
||||
"music_genre", "length", "fire_station:type:FR", "cadet", "observatory:type", "tower:type",
|
||||
"zoo", "shooting", "commons", "groomer", "group_only", "hazard", "identity", "interaction",
|
||||
"logo", "maxheight", "provides", "regional", "scale", "site", "plots", "allotments",
|
||||
"local_food", "monitoring:pedestrian", "recording:automated", "yacht", "background_music",
|
||||
"url:spaceapi", "openfire", "fraternity(:.*)?",
|
||||
// misc specific attributes
|
||||
"clothes", "shoes", "tailor", "beauty", "tobacco", "carpenter", "furniture", "lottery",
|
||||
"sport", "dispensing", "tailor:.*", "gambling", "material", "raw_material", "stonemason",
|
||||
"studio", "scuba_diving(:.*)?", "polling_station", "collector", "books", "agrarian",
|
||||
"musical_instrument", "massage", "parts", "post_office(:.*)?", "religion", "denomination",
|
||||
"rental", ".*:rental", "tickets:.*", "public_transport", "goods_supply", "pet", "appliance",
|
||||
"artwork_type", "charity", "company", "crop", "dry_cleaning", "factory", "feature",
|
||||
"air_conditioning", "atm", "vending", "vending_machine", "recycling_type", "museum",
|
||||
"license_classes", "dance:.*", "isced:level", "school", "preschool", "university",
|
||||
"research_institution", "research", "member_of", "topic", "townhall:type", "parish", "police",
|
||||
"government", "thw:(lv|rb|ltg)", "office", "administration", "administrative", "association",
|
||||
"transport", "utility", "consulting", "Commercial", "commercial", "private", "taxi",
|
||||
"admin_level", "official_status", "target", "liaison", "diplomatic(:.*)?", "embassy",
|
||||
"consulate", "aeroway", "department", "faculty", "aerospace:product", "boundary", "population",
|
||||
"diocese", "depot", "cargo", "function", "game", "party", "political_party.*",
|
||||
"telecom(munication)?", "service_times", "kitchen:facilities", "it:(type|sales)",
|
||||
"cannabis:cbd", "bath:type", "bath:(open_air|sand_bath)", "animal_boarding", "animal_shelter",
|
||||
"mattress", "screen", "monitoring:weather", "public", "theatre", "culture", "library",
|
||||
"cooperative(:.*)?", "winery", "curtain", "lawyer(:.*)?", "local_authority(:.*)?", "equipment",
|
||||
"hackerspace",
|
||||
"camp_site", "camping", "bbq", "static_caravans", "emergency(:.*)?", "evacuation_cent(er|re)",
|
||||
"education", "engineering", "forestry", "foundation", "lawyer", "logistics", "military",
|
||||
"community_centre", "bank", "operational", "users_(PLWD|boy|elderly|female|girl|men)",
|
||||
"Comments?", "comments?", "entrance:(width|step_count|kerb:height)", "fenced", "motor_vehicle",
|
||||
"shelter",
|
||||
};
|
||||
6
libs/editor/module.modulemap
Normal file
6
libs/editor/module.modulemap
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module OSMEditor {
|
||||
header "osm_auth.hpp"
|
||||
header "osm_editor.hpp"
|
||||
header "server_api.hpp"
|
||||
requires cplusplus
|
||||
}
|
||||
63
libs/editor/new_feature_categories.cpp
Normal file
63
libs/editor/new_feature_categories.cpp
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
#include "new_feature_categories.hpp"
|
||||
|
||||
#include "indexer/categories_holder.hpp"
|
||||
#include "indexer/classificator.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace osm
|
||||
{
|
||||
NewFeatureCategories::NewFeatureCategories(editor::EditorConfig const & config)
|
||||
{
|
||||
Classificator const & c = classif();
|
||||
for (auto const & clType : config.GetTypesThatCanBeAdded())
|
||||
{
|
||||
uint32_t const type = c.GetTypeByReadableObjectName(clType);
|
||||
if (type == 0)
|
||||
{
|
||||
LOG(LWARNING, ("Unknown type in Editor's config:", clType));
|
||||
continue;
|
||||
}
|
||||
m_types.emplace_back(clType);
|
||||
}
|
||||
}
|
||||
|
||||
NewFeatureCategories::NewFeatureCategories(NewFeatureCategories && other) noexcept
|
||||
: m_index(std::move(other.m_index))
|
||||
, m_types(std::move(other.m_types))
|
||||
{
|
||||
// Do not move m_addedLangs, see Framework::GetEditorCategories() usage.
|
||||
}
|
||||
|
||||
void NewFeatureCategories::AddLanguage(std::string lang)
|
||||
{
|
||||
auto langCode = CategoriesHolder::MapLocaleToInteger(lang);
|
||||
if (langCode == CategoriesHolder::kUnsupportedLocaleCode)
|
||||
{
|
||||
lang = "en";
|
||||
langCode = CategoriesHolder::kEnglishCode;
|
||||
}
|
||||
if (m_addedLangs.Contains(langCode))
|
||||
return;
|
||||
|
||||
auto const & c = classif();
|
||||
for (auto const & type : m_types)
|
||||
m_index.AddCategoryByTypeAndLang(c.GetTypeByReadableObjectName(type), langCode);
|
||||
|
||||
m_addedLangs.Insert(langCode);
|
||||
}
|
||||
|
||||
NewFeatureCategories::TypeNames NewFeatureCategories::Search(std::string const & query) const
|
||||
{
|
||||
std::vector<uint32_t> resultTypes;
|
||||
m_index.GetAssociatedTypes(query, resultTypes);
|
||||
|
||||
auto const & c = classif();
|
||||
NewFeatureCategories::TypeNames result(resultTypes.size());
|
||||
for (size_t i = 0; i < result.size(); ++i)
|
||||
result[i] = c.GetReadableObjectName(resultTypes[i]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace osm
|
||||
54
libs/editor/new_feature_categories.hpp
Normal file
54
libs/editor/new_feature_categories.hpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
#pragma once
|
||||
|
||||
#include "editor/editor_config.hpp"
|
||||
|
||||
#include "indexer/categories_holder.hpp"
|
||||
#include "indexer/categories_index.hpp"
|
||||
|
||||
#include "base/macros.hpp"
|
||||
#include "base/small_set.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace osm
|
||||
{
|
||||
// This class holds an index of categories that can be set for a newly added feature.
|
||||
class NewFeatureCategories
|
||||
{
|
||||
public:
|
||||
using TypeName = std::string;
|
||||
using TypeNames = std::vector<TypeName>;
|
||||
|
||||
NewFeatureCategories() = default;
|
||||
explicit NewFeatureCategories(editor::EditorConfig const & config);
|
||||
|
||||
NewFeatureCategories(NewFeatureCategories && other) noexcept;
|
||||
NewFeatureCategories & operator=(NewFeatureCategories && other) = default;
|
||||
|
||||
// Adds all known synonyms in language |lang| for all categories that
|
||||
// can be applied to a newly added feature.
|
||||
// If one language is added more than once, all the calls except for the
|
||||
// first one are ignored.
|
||||
// If |lang| is not supported, "en" is used.
|
||||
void AddLanguage(std::string lang);
|
||||
|
||||
// Returns names (in language |queryLang|) and types of categories that have a synonym containing
|
||||
// the substring |query| (in any language that was added before).
|
||||
// If |lang| is not supported, "en" is used.
|
||||
// The returned list is sorted.
|
||||
TypeNames Search(std::string const & query) const;
|
||||
|
||||
// Returns all registered classifier category types (GetReadableObjectName).
|
||||
TypeNames const & GetAllCreatableTypeNames() const { return m_types; }
|
||||
|
||||
private:
|
||||
using Langs = base::SmallSet<CategoriesHolder::kLocaleMapping.size() + 1>;
|
||||
|
||||
indexer::CategoriesIndex m_index;
|
||||
Langs m_addedLangs;
|
||||
TypeNames m_types;
|
||||
|
||||
DISALLOW_COPY(NewFeatureCategories);
|
||||
};
|
||||
} // namespace osm
|
||||
377
libs/editor/opening_hours_ui.cpp
Normal file
377
libs/editor/opening_hours_ui.cpp
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
#include "opening_hours_ui.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace editor::ui;
|
||||
|
||||
size_t SpanLength(osmoh::Timespan const & span)
|
||||
{
|
||||
using osmoh::operator""_h;
|
||||
auto const start = span.GetStart().GetHourMinutes().GetDurationCount();
|
||||
auto const end = span.GetEnd().GetHourMinutes().GetDurationCount();
|
||||
return end - start + (span.HasExtendedHours() ? osmoh::HourMinutes::TMinutes(24_h).count() : 0);
|
||||
}
|
||||
|
||||
bool DoesIncludeAll(osmoh::Timespan const & openingTime, osmoh::TTimespans const & spans)
|
||||
{
|
||||
if (spans.empty())
|
||||
return true;
|
||||
|
||||
auto const openingTimeStart = openingTime.GetStart().GetHourMinutes().GetDuration();
|
||||
auto const openingTimeEnd = openingTime.GetEnd().GetHourMinutes().GetDuration();
|
||||
auto const excludeTimeStart = spans.front().GetStart().GetHourMinutes().GetDuration();
|
||||
auto const excludeTimeEnd = spans.back().GetEnd().GetHourMinutes().GetDuration();
|
||||
|
||||
if (!openingTime.HasExtendedHours() && (excludeTimeStart < openingTimeStart || openingTimeEnd < excludeTimeEnd))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FixTimeSpans(osmoh::Timespan openingTime, osmoh::TTimespans & spans)
|
||||
{
|
||||
using osmoh::operator""_h;
|
||||
|
||||
if (spans.empty())
|
||||
return true;
|
||||
|
||||
for (auto & span : spans)
|
||||
if (span.HasExtendedHours())
|
||||
span.GetEnd().GetHourMinutes().AddDuration(24_h);
|
||||
|
||||
std::sort(std::begin(spans), std::end(spans), [](osmoh::Timespan const & s1, osmoh::Timespan const s2)
|
||||
{
|
||||
auto const start1 = s1.GetStart().GetHourMinutes();
|
||||
auto const start2 = s2.GetStart().GetHourMinutes();
|
||||
|
||||
// If two spans start at the same point the longest span should be leftmost.
|
||||
if (start1 == start2)
|
||||
return SpanLength(s1) > SpanLength(s2);
|
||||
|
||||
return start1 < start2;
|
||||
});
|
||||
|
||||
osmoh::TTimespans result{spans.front()};
|
||||
for (size_t i = 1, j = 0; i < spans.size(); ++i)
|
||||
{
|
||||
auto const start2 = spans[i].GetStart().GetHourMinutes().GetDuration();
|
||||
auto const end1 = spans[j].GetEnd().GetHourMinutes().GetDuration();
|
||||
auto const end2 = spans[i].GetEnd().GetHourMinutes().GetDuration();
|
||||
|
||||
// The first one includes the second.
|
||||
if (start2 < end1 && end2 <= end1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Two spans have non-empty intersection.
|
||||
else if (start2 <= end1)
|
||||
{
|
||||
result.back().SetEnd(spans[i].GetEnd());
|
||||
}
|
||||
// The scond span starts after the end of the first one.
|
||||
else
|
||||
{
|
||||
result.push_back(spans[i]);
|
||||
++j;
|
||||
}
|
||||
}
|
||||
|
||||
// Check that all exclude time spans are included in opening time.
|
||||
if (openingTime.HasExtendedHours())
|
||||
openingTime.GetEnd().GetHourMinutes().AddDuration(24_h);
|
||||
|
||||
if (!DoesIncludeAll(openingTime, spans))
|
||||
return false;
|
||||
|
||||
for (auto & span : result)
|
||||
if (span.HasExtendedHours())
|
||||
span.GetEnd().GetHourMinutes().AddDuration(-24_h);
|
||||
|
||||
spans.swap(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
osmoh::Timespan GetLongetsOpenSpan(osmoh::Timespan const & openingTime, osmoh::TTimespans const & excludeTime)
|
||||
{
|
||||
if (excludeTime.empty())
|
||||
return openingTime;
|
||||
|
||||
osmoh::Timespan longestSpan{openingTime.GetStart(), excludeTime.front().GetStart()};
|
||||
for (size_t i = 0; i + 1 < excludeTime.size(); ++i)
|
||||
{
|
||||
osmoh::Timespan nextOpenSpan{excludeTime[i].GetEnd(), excludeTime[i + 1].GetStart()};
|
||||
longestSpan = SpanLength(longestSpan) > SpanLength(nextOpenSpan) ? longestSpan : nextOpenSpan;
|
||||
}
|
||||
|
||||
osmoh::Timespan lastSpan{excludeTime.back().GetEnd(), openingTime.GetEnd()};
|
||||
return SpanLength(longestSpan) > SpanLength(lastSpan) ? longestSpan : lastSpan;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace editor
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
|
||||
// TimeTable ---------------------------------------------------------------------------------------
|
||||
|
||||
TimeTable TimeTable::GetPredefinedTimeTable()
|
||||
{
|
||||
TimeTable tt;
|
||||
tt.m_isTwentyFourHours = true;
|
||||
tt.m_weekdays = {osmoh::Weekday::Sunday, osmoh::Weekday::Monday, osmoh::Weekday::Tuesday, osmoh::Weekday::Wednesday,
|
||||
osmoh::Weekday::Thursday, osmoh::Weekday::Friday, osmoh::Weekday::Saturday};
|
||||
|
||||
tt.m_openingTime = tt.GetPredefinedOpeningTime();
|
||||
|
||||
return tt;
|
||||
}
|
||||
|
||||
bool TimeTable::SetOpeningDays(OpeningDays const & days)
|
||||
{
|
||||
if (days.empty())
|
||||
return false;
|
||||
m_weekdays = days;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TimeTable::AddWorkingDay(osmoh::Weekday const wd)
|
||||
{
|
||||
if (wd != osmoh::Weekday::None)
|
||||
m_weekdays.insert(wd);
|
||||
}
|
||||
|
||||
bool TimeTable::RemoveWorkingDay(osmoh::Weekday const wd)
|
||||
{
|
||||
if (m_weekdays.size() == 1)
|
||||
return false;
|
||||
m_weekdays.erase(wd);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TimeTable::SetOpeningTime(osmoh::Timespan const & span)
|
||||
{
|
||||
if (IsTwentyFourHours())
|
||||
return false;
|
||||
|
||||
m_openingTime = span;
|
||||
osmoh::TTimespans excludeTime;
|
||||
for (auto const & excludeSpan : GetExcludeTime())
|
||||
{
|
||||
auto const openingTimeStart = GetOpeningTime().GetStart().GetHourMinutes().GetDuration();
|
||||
auto const openingTimeEnd = GetOpeningTime().GetEnd().GetHourMinutes().GetDuration();
|
||||
auto const excludeSpanStart = excludeSpan.GetStart().GetHourMinutes().GetDuration();
|
||||
auto const excludeSpanEnd = excludeSpan.GetEnd().GetHourMinutes().GetDuration();
|
||||
|
||||
if (!GetOpeningTime().HasExtendedHours() &&
|
||||
(excludeSpanStart < openingTimeStart || openingTimeEnd < excludeSpanEnd))
|
||||
continue;
|
||||
|
||||
excludeTime.push_back(excludeSpan);
|
||||
}
|
||||
|
||||
m_excludeTime.swap(excludeTime);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TimeTable::CanAddExcludeTime() const
|
||||
{
|
||||
auto copy = *this;
|
||||
return copy.AddExcludeTime(GetPredefinedExcludeTime()) && copy.GetExcludeTime().size() == GetExcludeTime().size() + 1;
|
||||
}
|
||||
|
||||
bool TimeTable::AddExcludeTime(osmoh::Timespan const & span)
|
||||
{
|
||||
return ReplaceExcludeTime(span, GetExcludeTime().size());
|
||||
}
|
||||
|
||||
bool TimeTable::ReplaceExcludeTime(osmoh::Timespan const & span, size_t const index)
|
||||
{
|
||||
if (IsTwentyFourHours() || index > m_excludeTime.size())
|
||||
return false;
|
||||
|
||||
auto copy = m_excludeTime;
|
||||
if (index == m_excludeTime.size())
|
||||
copy.push_back(span);
|
||||
else
|
||||
copy[index] = span;
|
||||
|
||||
if (!FixTimeSpans(m_openingTime, copy))
|
||||
return false;
|
||||
|
||||
m_excludeTime.swap(copy);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TimeTable::RemoveExcludeTime(size_t const index)
|
||||
{
|
||||
if (IsTwentyFourHours() || index >= m_excludeTime.size())
|
||||
return false;
|
||||
m_excludeTime.erase(begin(m_excludeTime) + index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TimeTable::IsValid() const
|
||||
{
|
||||
if (m_weekdays.empty())
|
||||
return false;
|
||||
|
||||
if (!IsTwentyFourHours())
|
||||
{
|
||||
if (m_openingTime.IsEmpty())
|
||||
return false;
|
||||
|
||||
auto copy = GetExcludeTime();
|
||||
if (!FixTimeSpans(m_openingTime, copy))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
osmoh::Timespan TimeTable::GetPredefinedOpeningTime() const
|
||||
{
|
||||
using osmoh::operator""_h;
|
||||
return {9_h, 18_h};
|
||||
}
|
||||
|
||||
osmoh::Timespan TimeTable::GetPredefinedExcludeTime() const
|
||||
{
|
||||
using osmoh::operator""_h;
|
||||
using osmoh::operator""_min;
|
||||
using osmoh::HourMinutes;
|
||||
|
||||
auto longestOpenSpan = GetLongetsOpenSpan(GetOpeningTime(), GetExcludeTime());
|
||||
|
||||
auto const startTime = longestOpenSpan.GetStart().GetHourMinutes().GetDuration();
|
||||
auto const endTime = longestOpenSpan.GetEnd().GetHourMinutes().GetDuration();
|
||||
// We do not support exclude time spans in extended working intervals.
|
||||
if (endTime < startTime)
|
||||
return {};
|
||||
|
||||
auto const startHours = longestOpenSpan.GetStart().GetHourMinutes().GetHours();
|
||||
auto const endHours = longestOpenSpan.GetEnd().GetHourMinutes().GetHours();
|
||||
|
||||
auto const period = endHours - startHours;
|
||||
|
||||
// Cannot calculate exclude time when working time is less than 3 hours.
|
||||
if (period < 3_h)
|
||||
return {};
|
||||
|
||||
auto excludeTimeStart = startHours + HourMinutes::THours(period.count() / 2);
|
||||
|
||||
CHECK(excludeTimeStart < 24_h, ());
|
||||
|
||||
longestOpenSpan.SetStart(HourMinutes(excludeTimeStart));
|
||||
longestOpenSpan.SetEnd(HourMinutes(excludeTimeStart + 1_h));
|
||||
|
||||
return longestOpenSpan;
|
||||
}
|
||||
|
||||
// TimeTableSet ------------------------------------------------------------------------------------
|
||||
|
||||
TimeTableSet::TimeTableSet()
|
||||
{
|
||||
m_table.push_back(TimeTable::GetPredefinedTimeTable());
|
||||
}
|
||||
|
||||
OpeningDays TimeTableSet::GetUnhandledDays() const
|
||||
{
|
||||
OpeningDays days = {osmoh::Weekday::Sunday, osmoh::Weekday::Monday, osmoh::Weekday::Tuesday,
|
||||
osmoh::Weekday::Wednesday, osmoh::Weekday::Thursday, osmoh::Weekday::Friday,
|
||||
osmoh::Weekday::Saturday};
|
||||
|
||||
for (auto const & tt : *this)
|
||||
for (auto const day : tt.GetOpeningDays())
|
||||
days.erase(day);
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
TimeTable TimeTableSet::GetComplementTimeTable() const
|
||||
{
|
||||
TimeTable tt = TimeTable::GetUninitializedTimeTable();
|
||||
// Set predefined opening time before set 24 hours, otherwise
|
||||
// it has no effect.
|
||||
tt.SetTwentyFourHours(false);
|
||||
tt.SetOpeningTime(tt.GetPredefinedOpeningTime());
|
||||
tt.SetTwentyFourHours(true);
|
||||
tt.SetOpeningDays(GetUnhandledDays());
|
||||
return tt;
|
||||
}
|
||||
|
||||
bool TimeTableSet::IsTwentyFourPerSeven() const
|
||||
{
|
||||
return GetUnhandledDays().empty() && std::all_of(std::begin(m_table), std::end(m_table),
|
||||
[](TimeTable const & tt) { return tt.IsTwentyFourHours(); });
|
||||
}
|
||||
|
||||
bool TimeTableSet::Append(TimeTable const & tt)
|
||||
{
|
||||
auto copy = *this;
|
||||
copy.m_table.push_back(tt);
|
||||
|
||||
if (!TimeTableSet::UpdateByIndex(copy, copy.Size() - 1))
|
||||
return false;
|
||||
|
||||
m_table.swap(copy.m_table);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TimeTableSet::Remove(size_t const index)
|
||||
{
|
||||
if (Size() == 1 || index >= Size())
|
||||
return false;
|
||||
|
||||
m_table.erase(m_table.begin() + index);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TimeTableSet::Replace(TimeTable const & tt, size_t const index)
|
||||
{
|
||||
if (index >= Size())
|
||||
return false;
|
||||
|
||||
auto copy = *this;
|
||||
copy.m_table[index] = tt;
|
||||
|
||||
if (!TimeTableSet::UpdateByIndex(copy, index))
|
||||
return false;
|
||||
|
||||
m_table.swap(copy.m_table);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TimeTableSet::UpdateByIndex(TimeTableSet & ttSet, size_t const index)
|
||||
{
|
||||
auto const & updated = ttSet.m_table[index];
|
||||
|
||||
if (index >= ttSet.Size() || !updated.IsValid())
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i < ttSet.Size(); ++i)
|
||||
{
|
||||
if (i == index)
|
||||
continue;
|
||||
|
||||
auto && tt = ttSet.m_table[i];
|
||||
// Remove all days of updated timetable from all other timetables.
|
||||
OpeningDays days;
|
||||
std::set_difference(std::begin(tt.GetOpeningDays()), std::end(tt.GetOpeningDays()),
|
||||
std::begin(updated.GetOpeningDays()), std::end(updated.GetOpeningDays()),
|
||||
inserter(days, std::end(days)));
|
||||
|
||||
if (!tt.SetOpeningDays(days))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace ui
|
||||
} // namespace editor
|
||||
98
libs/editor/opening_hours_ui.hpp
Normal file
98
libs/editor/opening_hours_ui.hpp
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include "3party/opening_hours/opening_hours.hpp"
|
||||
|
||||
namespace editor
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
using OpeningDays = std::set<osmoh::Weekday>;
|
||||
|
||||
class TimeTable
|
||||
{
|
||||
public:
|
||||
static TimeTable GetUninitializedTimeTable() { return {}; }
|
||||
static TimeTable GetPredefinedTimeTable();
|
||||
|
||||
bool IsTwentyFourHours() const { return m_isTwentyFourHours; }
|
||||
void SetTwentyFourHours(bool const on) { m_isTwentyFourHours = on; }
|
||||
|
||||
OpeningDays const & GetOpeningDays() const { return m_weekdays; }
|
||||
bool SetOpeningDays(OpeningDays const & days);
|
||||
|
||||
void AddWorkingDay(osmoh::Weekday const wd);
|
||||
bool RemoveWorkingDay(osmoh::Weekday const wd);
|
||||
|
||||
osmoh::Timespan const & GetOpeningTime() const { return m_openingTime; }
|
||||
bool SetOpeningTime(osmoh::Timespan const & span);
|
||||
|
||||
bool CanAddExcludeTime() const;
|
||||
bool AddExcludeTime(osmoh::Timespan const & span);
|
||||
bool ReplaceExcludeTime(osmoh::Timespan const & span, size_t const index);
|
||||
bool RemoveExcludeTime(size_t const index);
|
||||
osmoh::TTimespans const & GetExcludeTime() const { return m_excludeTime; }
|
||||
|
||||
bool IsValid() const;
|
||||
|
||||
osmoh::Timespan GetPredefinedOpeningTime() const;
|
||||
osmoh::Timespan GetPredefinedExcludeTime() const;
|
||||
|
||||
private:
|
||||
TimeTable() = default;
|
||||
|
||||
bool m_isTwentyFourHours;
|
||||
OpeningDays m_weekdays;
|
||||
osmoh::Timespan m_openingTime;
|
||||
osmoh::TTimespans m_excludeTime;
|
||||
};
|
||||
|
||||
class TimeTableSet
|
||||
{
|
||||
using TimeTableSetImpl = std::vector<TimeTable>;
|
||||
|
||||
public:
|
||||
class Proxy : public TimeTable
|
||||
{
|
||||
public:
|
||||
Proxy(TimeTableSet & tts, size_t const index, TimeTable const & tt) : TimeTable(tt), m_index(index), m_tts(tts) {}
|
||||
|
||||
bool Commit() { return m_tts.Replace(*this, m_index); } // Slice base class on copy.
|
||||
|
||||
private:
|
||||
size_t const m_index;
|
||||
TimeTableSet & m_tts;
|
||||
};
|
||||
|
||||
TimeTableSet();
|
||||
|
||||
OpeningDays GetUnhandledDays() const;
|
||||
|
||||
TimeTable GetComplementTimeTable() const;
|
||||
|
||||
Proxy Get(size_t const index) { return Proxy(*this, index, m_table[index]); }
|
||||
Proxy Front() { return Get(0); }
|
||||
Proxy Back() { return Get(Size() - 1); }
|
||||
|
||||
size_t Size() const { return m_table.size(); }
|
||||
bool Empty() const { return m_table.empty(); }
|
||||
|
||||
bool IsTwentyFourPerSeven() const;
|
||||
|
||||
bool Append(TimeTable const & tt);
|
||||
bool Remove(size_t const index);
|
||||
|
||||
bool Replace(TimeTable const & tt, size_t const index);
|
||||
|
||||
TimeTableSetImpl::const_iterator begin() const { return m_table.begin(); }
|
||||
TimeTableSetImpl::const_iterator end() const { return m_table.end(); }
|
||||
|
||||
private:
|
||||
static bool UpdateByIndex(TimeTableSet & ttSet, size_t const index);
|
||||
|
||||
TimeTableSetImpl m_table;
|
||||
};
|
||||
} // namespace ui
|
||||
} // namespace editor
|
||||
418
libs/editor/osm_auth.cpp
Normal file
418
libs/editor/osm_auth.cpp
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
#include "editor/osm_auth.hpp"
|
||||
|
||||
#include "platform/http_client.hpp"
|
||||
|
||||
#include "coding/url.hpp"
|
||||
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include "cppjansson/cppjansson.hpp"
|
||||
|
||||
#include "private.h"
|
||||
|
||||
namespace osm
|
||||
{
|
||||
using platform::HttpClient;
|
||||
using std::string;
|
||||
|
||||
constexpr char const * kApiVersion = "/api/0.6";
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
string FindAuthenticityToken(string const & body)
|
||||
{
|
||||
auto pos = body.find("name=\"authenticity_token\"");
|
||||
if (pos == string::npos)
|
||||
return {};
|
||||
string const kValue = "value=\"";
|
||||
auto start = body.find(kValue, pos);
|
||||
if (start == string::npos)
|
||||
return {};
|
||||
start += kValue.length();
|
||||
auto const end = body.find('"', start);
|
||||
return end == string::npos ? string() : body.substr(start, end - start);
|
||||
}
|
||||
|
||||
// Parse URL in format "{OSM_OAUTH2_REDIRECT_URI}?code=XXXX". Extract code value
|
||||
string FindOauthCode(string const & redirectUri)
|
||||
{
|
||||
auto const url = url::Url::FromString(redirectUri);
|
||||
string const * oauth2code = url.GetParamValue("code");
|
||||
|
||||
if (!oauth2code || oauth2code->empty())
|
||||
return {};
|
||||
|
||||
return *oauth2code;
|
||||
}
|
||||
|
||||
string FindAccessToken(string const & body)
|
||||
{
|
||||
// Extract access_token from JSON in format {"access_token":"...", "token_type":"Bearer", "scope":"read_prefs"}
|
||||
base::Json const root(body.c_str());
|
||||
|
||||
if (json_is_object(root.get()))
|
||||
{
|
||||
json_t * token_node = json_object_get(root.get(), "access_token");
|
||||
if (json_is_string(token_node))
|
||||
return json_string_value(token_node);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
string BuildPostRequest(std::initializer_list<std::pair<string, string>> const & params)
|
||||
{
|
||||
string result;
|
||||
for (auto it = params.begin(); it != params.end(); ++it)
|
||||
{
|
||||
if (it != params.begin())
|
||||
result += "&";
|
||||
result += it->first + "=" + url::UrlEncode(it->second);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// static
|
||||
bool OsmOAuth::IsValid(string const & ks)
|
||||
{
|
||||
return !ks.empty();
|
||||
}
|
||||
|
||||
OsmOAuth::OsmOAuth(string const & oauth2ClientId, string const & oauth2Scope, string const & oauth2RedirectUri,
|
||||
string baseUrl, string apiUrl)
|
||||
: m_oauth2params{oauth2ClientId, oauth2Scope, oauth2RedirectUri}
|
||||
, m_baseUrl(std::move(baseUrl))
|
||||
, m_apiUrl(std::move(apiUrl))
|
||||
{}
|
||||
// static
|
||||
OsmOAuth OsmOAuth::ServerAuth()
|
||||
{
|
||||
#ifdef DEBUG
|
||||
return DevServerAuth();
|
||||
#else
|
||||
return ProductionServerAuth();
|
||||
#endif
|
||||
}
|
||||
// static
|
||||
OsmOAuth OsmOAuth::ServerAuth(string const & oauthToken)
|
||||
{
|
||||
OsmOAuth auth = ServerAuth();
|
||||
auth.SetAuthToken(oauthToken);
|
||||
return auth;
|
||||
}
|
||||
// static
|
||||
OsmOAuth OsmOAuth::DevServerAuth()
|
||||
{
|
||||
constexpr char const * kOsmDevServer = "https://master.apis.dev.openstreetmap.org";
|
||||
// CoMaps keys for OSM dev server
|
||||
constexpr char const * kOsmDevClientId = "Tj8yyx3FWy_N5wz6sUTAXTM6YBAiwVgM7sRLrLix2u8";
|
||||
constexpr char const * kOsmDevScope = "read_prefs write_api write_notes";
|
||||
constexpr char const * kOsmDevRedirectUri = "cm://oauth2/osm/callback";
|
||||
|
||||
return {kOsmDevClientId, kOsmDevScope, kOsmDevRedirectUri, kOsmDevServer, kOsmDevServer};
|
||||
}
|
||||
// static
|
||||
OsmOAuth OsmOAuth::ProductionServerAuth()
|
||||
{
|
||||
constexpr char const * kOsmMainSiteURL = "https://www.openstreetmap.org";
|
||||
constexpr char const * kOsmApiURL = "https://api.openstreetmap.org";
|
||||
return {OSM_OAUTH2_CLIENT_ID, OSM_OAUTH2_SCOPE, OSM_OAUTH2_REDIRECT_URI, kOsmMainSiteURL, kOsmApiURL};
|
||||
}
|
||||
|
||||
void OsmOAuth::SetAuthToken(string const & oauthToken)
|
||||
{
|
||||
m_oauth2token = oauthToken;
|
||||
}
|
||||
|
||||
string const & OsmOAuth::GetAuthToken() const
|
||||
{
|
||||
return m_oauth2token;
|
||||
}
|
||||
|
||||
bool OsmOAuth::IsAuthorized() const
|
||||
{
|
||||
return IsValid(m_oauth2token);
|
||||
}
|
||||
|
||||
// Opens a login page and extract a cookie and a secret token.
|
||||
OsmOAuth::SessionID OsmOAuth::FetchSessionId(string const & subUrl, string const & cookies) const
|
||||
{
|
||||
string const url = m_baseUrl + subUrl + (cookies.empty() ? "?cookie_test=true" : "");
|
||||
HttpClient request(url);
|
||||
request.SetCookies(cookies);
|
||||
if (!request.RunHttpRequest())
|
||||
MYTHROW(NetworkError, ("FetchSessionId Network error while connecting to", url));
|
||||
if (request.WasRedirected())
|
||||
MYTHROW(UnexpectedRedirect, ("FetchSessionId Unexpected redirected to", request.UrlReceived(), "from", url));
|
||||
if (request.ErrorCode() != HTTP::OK)
|
||||
MYTHROW(FetchSessionIdError, (DebugPrint(request)));
|
||||
|
||||
SessionID sid = {request.CombinedCookies(), FindAuthenticityToken(request.ServerResponse())};
|
||||
if (sid.m_cookies.empty() || sid.m_authenticityToken.empty())
|
||||
MYTHROW(FetchSessionIdError, ("Cookies and/or token are empty for request", DebugPrint(request)));
|
||||
return sid;
|
||||
}
|
||||
|
||||
void OsmOAuth::LogoutUser(SessionID const & sid) const
|
||||
{
|
||||
HttpClient request(m_baseUrl + "/logout");
|
||||
request.SetCookies(sid.m_cookies);
|
||||
if (!request.RunHttpRequest())
|
||||
MYTHROW(NetworkError, ("LogoutUser Network error while connecting to", request.UrlRequested()));
|
||||
if (request.ErrorCode() != HTTP::OK)
|
||||
MYTHROW(LogoutUserError, (DebugPrint(request)));
|
||||
}
|
||||
|
||||
bool OsmOAuth::LoginUserPassword(string const & login, string const & password, SessionID const & sid) const
|
||||
{
|
||||
auto params = BuildPostRequest({{"username", login},
|
||||
{"password", password},
|
||||
{"referer", "/"},
|
||||
{"commit", "Login"},
|
||||
{"authenticity_token", sid.m_authenticityToken}});
|
||||
HttpClient request(m_baseUrl + "/login");
|
||||
request.SetBodyData(std::move(params), "application/x-www-form-urlencoded")
|
||||
.SetCookies(sid.m_cookies)
|
||||
.SetFollowRedirects(true);
|
||||
if (!request.RunHttpRequest())
|
||||
MYTHROW(NetworkError, ("LoginUserPassword Network error while connecting to", request.UrlRequested()));
|
||||
|
||||
// At the moment, automatic redirects handling is buggy on Androids < 4.4.
|
||||
// set_follow_redirects(false) works only for Android and iOS, while curl still automatically follow all redirects.
|
||||
if (request.ErrorCode() != HTTP::OK && request.ErrorCode() != HTTP::Found)
|
||||
MYTHROW(LoginUserPasswordServerError, (DebugPrint(request)));
|
||||
|
||||
// Not redirected page is a 100% signal that login and/or password are invalid.
|
||||
if (!request.WasRedirected())
|
||||
return false;
|
||||
|
||||
// Check if we were redirected to some 3rd party site.
|
||||
if (request.UrlReceived().find(m_baseUrl) != 0)
|
||||
MYTHROW(UnexpectedRedirect, (DebugPrint(request)));
|
||||
|
||||
// m_baseUrl + "/login" means login and/or password are invalid.
|
||||
return request.ServerResponse().find("/login") == string::npos;
|
||||
}
|
||||
|
||||
bool OsmOAuth::LoginSocial(string const & callbackPart, string const & socialToken, SessionID const & sid) const
|
||||
{
|
||||
string const url = m_baseUrl + callbackPart + socialToken;
|
||||
HttpClient request(url);
|
||||
request.SetCookies(sid.m_cookies).SetFollowRedirects(true);
|
||||
if (!request.RunHttpRequest())
|
||||
MYTHROW(NetworkError, ("LoginSocial Network error while connecting to", request.UrlRequested()));
|
||||
if (request.ErrorCode() != HTTP::OK && request.ErrorCode() != HTTP::Found)
|
||||
MYTHROW(LoginSocialServerError, (DebugPrint(request)));
|
||||
|
||||
// Not redirected page is a 100% signal that social login has failed.
|
||||
if (!request.WasRedirected())
|
||||
return false;
|
||||
|
||||
// Check if we were redirected to some 3rd party site.
|
||||
if (request.UrlReceived().find(m_baseUrl) != 0)
|
||||
MYTHROW(UnexpectedRedirect, (DebugPrint(request)));
|
||||
|
||||
// m_baseUrl + "/login" means login and/or password are invalid.
|
||||
return request.ServerResponse().find("/login") == string::npos;
|
||||
}
|
||||
|
||||
// Fakes a buttons press to automatically accept requested permissions.
|
||||
string OsmOAuth::SendAuthRequest(string const & requestTokenKey, SessionID const & lastSid) const
|
||||
{
|
||||
auto params = BuildPostRequest({{"authenticity_token", requestTokenKey},
|
||||
{"client_id", m_oauth2params.m_clientId},
|
||||
{"redirect_uri", m_oauth2params.m_redirectUri},
|
||||
{"scope", m_oauth2params.m_scope},
|
||||
{"response_type", "code"}});
|
||||
HttpClient request(m_baseUrl + "/oauth2/authorize");
|
||||
request.SetBodyData(std::move(params), "application/x-www-form-urlencoded")
|
||||
.SetCookies(lastSid.m_cookies)
|
||||
//.SetRawHeader("Origin", m_baseUrl)
|
||||
.SetFollowRedirects(false);
|
||||
if (!request.RunHttpRequest())
|
||||
MYTHROW(NetworkError, ("SendAuthRequest Network error while connecting to", request.UrlRequested()));
|
||||
if (!request.WasRedirected())
|
||||
MYTHROW(UnexpectedRedirect, ("Expected redirect for URL", request.UrlRequested()));
|
||||
|
||||
// Recieved URL in format "{OSM_OAUTH2_REDIRECT_URI}?code=XXXX". Extract code value
|
||||
string const oauthCode = FindOauthCode(request.UrlReceived());
|
||||
if (oauthCode.empty())
|
||||
MYTHROW(OsmOAuth::NetworkError, ("SendAuthRequest Redirect url has no 'code' parameter", request.UrlReceived()));
|
||||
return oauthCode;
|
||||
}
|
||||
|
||||
string OsmOAuth::FetchRequestToken(SessionID const & sid) const
|
||||
{
|
||||
HttpClient request(BuildOAuth2Url());
|
||||
request.SetCookies(sid.m_cookies).SetFollowRedirects(false);
|
||||
|
||||
if (!request.RunHttpRequest())
|
||||
MYTHROW(NetworkError, ("FetchRequestToken Network error while connecting to", request.UrlRequested()));
|
||||
|
||||
if (request.WasRedirected())
|
||||
{
|
||||
if (request.UrlReceived().find(m_oauth2params.m_redirectUri) != 0)
|
||||
MYTHROW(OsmOAuth::NetworkError, ("FetchRequestToken Redirect url han unexpected prefix", request.UrlReceived()));
|
||||
|
||||
// User already accepted OAuth2 request.
|
||||
// Recieved URL in format "{OSM_OAUTH2_REDIRECT_URI}?code=XXXX". Extract code value
|
||||
string const oauthCode = FindOauthCode(request.UrlReceived());
|
||||
if (oauthCode.empty())
|
||||
MYTHROW(OsmOAuth::NetworkError,
|
||||
("FetchRequestToken Redirect url has no 'code' parameter", request.UrlReceived()));
|
||||
return oauthCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (request.ErrorCode() != HTTP::OK)
|
||||
MYTHROW(FetchRequestTokenServerError, (DebugPrint(request)));
|
||||
|
||||
// Throws std::runtime_error.
|
||||
string const authenticityToken = FindAuthenticityToken(request.ServerResponse());
|
||||
|
||||
// Accept OAuth2 request from server
|
||||
return SendAuthRequest(authenticityToken, sid);
|
||||
}
|
||||
}
|
||||
|
||||
string OsmOAuth::BuildOAuth2Url() const
|
||||
{
|
||||
auto requestTokenUrl = m_baseUrl + "/oauth2/authorize";
|
||||
auto const requestTokenQuery = BuildPostRequest({{"client_id", m_oauth2params.m_clientId},
|
||||
{"redirect_uri", m_oauth2params.m_redirectUri},
|
||||
{"scope", m_oauth2params.m_scope},
|
||||
{"response_type", "code"}});
|
||||
return requestTokenUrl.append("?").append(requestTokenQuery);
|
||||
}
|
||||
|
||||
string OsmOAuth::FinishAuthorization(string const & oauth2code) const
|
||||
{
|
||||
auto params = BuildPostRequest({
|
||||
{"grant_type", "authorization_code"},
|
||||
{"code", oauth2code},
|
||||
{"client_id", m_oauth2params.m_clientId},
|
||||
{"redirect_uri", m_oauth2params.m_redirectUri},
|
||||
{"scope", m_oauth2params.m_scope},
|
||||
});
|
||||
|
||||
HttpClient request(m_baseUrl + "/oauth2/token");
|
||||
request.SetBodyData(std::move(params), "application/x-www-form-urlencoded").SetFollowRedirects(true);
|
||||
if (!request.RunHttpRequest())
|
||||
MYTHROW(NetworkError, ("FinishAuthorization Network error while connecting to", request.UrlRequested()));
|
||||
if (request.ErrorCode() != HTTP::OK)
|
||||
MYTHROW(FinishAuthorizationServerError, (DebugPrint(request)));
|
||||
if (request.WasRedirected())
|
||||
MYTHROW(UnexpectedRedirect, ("Redirected to", request.UrlReceived(), "from", request.UrlRequested()));
|
||||
|
||||
// Parse response JSON
|
||||
return FindAccessToken(request.ServerResponse());
|
||||
}
|
||||
|
||||
// Given a web session id, fetches an OAuth access token.
|
||||
string OsmOAuth::FetchAccessToken(SessionID const & sid) const
|
||||
{
|
||||
// Faking a button press for access rights.
|
||||
string const oauth2code = FetchRequestToken(sid);
|
||||
LogoutUser(sid);
|
||||
|
||||
// Got code, exchange it for the access token.
|
||||
return FinishAuthorization(oauth2code);
|
||||
}
|
||||
|
||||
bool OsmOAuth::AuthorizePassword(string const & login, string const & password)
|
||||
{
|
||||
SessionID const sid = FetchSessionId();
|
||||
if (!LoginUserPassword(login, password, sid))
|
||||
return false;
|
||||
m_oauth2token = FetchAccessToken(sid);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @todo OSM API to reset password has changed and should be updated
|
||||
/*
|
||||
bool OsmOAuth::ResetPassword(string const & email) const
|
||||
{
|
||||
string const kForgotPasswordUrlPart = "/user/forgot-password";
|
||||
|
||||
SessionID const sid = FetchSessionId(kForgotPasswordUrlPart);
|
||||
auto params = BuildPostRequest({
|
||||
{"email", email},
|
||||
{"authenticity_token", sid.m_authenticityToken},
|
||||
{"commit", "Reset password"},
|
||||
});
|
||||
HttpClient request(m_baseUrl + kForgotPasswordUrlPart);
|
||||
request.SetBodyData(std::move(params), "application/x-www-form-urlencoded");
|
||||
request.SetCookies(sid.m_cookies);
|
||||
request.SetHandleRedirects(false);
|
||||
|
||||
if (!request.RunHttpRequest())
|
||||
MYTHROW(NetworkError, ("ResetPassword Network error while connecting to", request.UrlRequested()));
|
||||
if (request.ErrorCode() != HTTP::OK)
|
||||
MYTHROW(ResetPasswordServerError, (DebugPrint(request)));
|
||||
|
||||
if (request.WasRedirected() && request.UrlReceived().find(m_baseUrl) != string::npos)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
|
||||
OsmOAuth::Response OsmOAuth::Request(string const & method, string const & httpMethod, string const & body) const
|
||||
{
|
||||
if (!IsValid(m_oauth2token))
|
||||
MYTHROW(InvalidKeySecret, ("Auth token is empty."));
|
||||
|
||||
string url = m_apiUrl + kApiVersion + method;
|
||||
|
||||
HttpClient request(url);
|
||||
request.SetRawHeader("Authorization", "Bearer " + m_oauth2token);
|
||||
|
||||
if (httpMethod != "GET")
|
||||
request.SetBodyData(body, "application/xml", httpMethod);
|
||||
if (!request.RunHttpRequest())
|
||||
MYTHROW(NetworkError, ("Request Network error while connecting to", url));
|
||||
if (request.WasRedirected())
|
||||
MYTHROW(UnexpectedRedirect, ("Redirected to", request.UrlReceived(), "from", url));
|
||||
|
||||
return {request.ErrorCode(), request.ServerResponse()};
|
||||
}
|
||||
|
||||
OsmOAuth::Response OsmOAuth::DirectRequest(string const & method, bool api) const
|
||||
{
|
||||
string const url = api ? m_apiUrl + kApiVersion + method : m_baseUrl + method;
|
||||
HttpClient request(url);
|
||||
if (!request.RunHttpRequest())
|
||||
MYTHROW(NetworkError, ("DirectRequest Network error while connecting to", url));
|
||||
if (request.WasRedirected())
|
||||
MYTHROW(UnexpectedRedirect, ("Redirected to", request.UrlReceived(), "from", url));
|
||||
|
||||
return {request.ErrorCode(), request.ServerResponse()};
|
||||
}
|
||||
|
||||
string DebugPrint(OsmOAuth::Response const & code)
|
||||
{
|
||||
string r;
|
||||
switch (code.first)
|
||||
{
|
||||
case OsmOAuth::HTTP::OK: r = "OK"; break;
|
||||
case OsmOAuth::HTTP::BadXML: r = "BadXML"; break;
|
||||
case OsmOAuth::HTTP::BadAuth: r = "BadAuth"; break;
|
||||
case OsmOAuth::HTTP::Redacted: r = "Redacted"; break;
|
||||
case OsmOAuth::HTTP::NotFound: r = "NotFound"; break;
|
||||
case OsmOAuth::HTTP::WrongMethod: r = "WrongMethod"; break;
|
||||
case OsmOAuth::HTTP::Conflict: r = "Conflict"; break;
|
||||
case OsmOAuth::HTTP::Gone: r = "Gone"; break;
|
||||
case OsmOAuth::HTTP::PreconditionFailed: r = "PreconditionFailed"; break;
|
||||
case OsmOAuth::HTTP::URITooLong: r = "URITooLong"; break;
|
||||
case OsmOAuth::HTTP::TooMuchData: r = "TooMuchData"; break;
|
||||
default:
|
||||
// No data from server in case of NetworkError.
|
||||
if (code.first < 0)
|
||||
return "NetworkError " + strings::to_string(code.first);
|
||||
r = "HTTP " + strings::to_string(code.first);
|
||||
}
|
||||
return r + ": " + code.second;
|
||||
}
|
||||
|
||||
} // namespace osm
|
||||
144
libs/editor/osm_auth.hpp
Normal file
144
libs/editor/osm_auth.hpp
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
#pragma once
|
||||
|
||||
#include "base/exception.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace osm
|
||||
{
|
||||
struct Oauth2Params
|
||||
{
|
||||
std::string m_clientId;
|
||||
std::string m_scope;
|
||||
std::string m_redirectUri;
|
||||
};
|
||||
|
||||
/// All methods that interact with the OSM server are blocking and not asynchronous.
|
||||
class OsmOAuth
|
||||
{
|
||||
public:
|
||||
/// Do not use enum class here for easier matching with error_code().
|
||||
enum HTTP : int
|
||||
{
|
||||
OK = 200,
|
||||
Found = 302,
|
||||
BadXML = 400,
|
||||
BadAuth = 401,
|
||||
Redacted = 403,
|
||||
NotFound = 404,
|
||||
WrongMethod = 405,
|
||||
Conflict = 409,
|
||||
Gone = 410,
|
||||
// Most often it means bad reference to another object.
|
||||
PreconditionFailed = 412,
|
||||
URITooLong = 414,
|
||||
TooMuchData = 509
|
||||
};
|
||||
|
||||
/// A pair of <http error code, response contents>.
|
||||
using Response = std::pair<int, std::string>;
|
||||
|
||||
DECLARE_EXCEPTION(OsmOAuthException, RootException);
|
||||
DECLARE_EXCEPTION(NetworkError, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(UnexpectedRedirect, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(UnsupportedApiRequestMethod, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(InvalidKeySecret, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(FetchSessionIdError, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(LogoutUserError, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(LoginUserPasswordServerError, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(LoginUserPasswordFailed, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(LoginSocialServerError, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(LoginSocialFailed, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(SendAuthRequestError, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(FetchRequestTokenServerError, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(FinishAuthorizationServerError, OsmOAuthException);
|
||||
DECLARE_EXCEPTION(ResetPasswordServerError, OsmOAuthException);
|
||||
|
||||
static bool IsValid(std::string const & ks);
|
||||
|
||||
/// The constructor. Simply stores a lot of strings in fields.
|
||||
OsmOAuth(std::string const & oauth2ClientId, std::string const & oauth2Scope, std::string const & oauth2RedirectUri,
|
||||
std::string baseUrl, std::string apiUrl);
|
||||
|
||||
/// Should be used everywhere in production code instead of servers below.
|
||||
static OsmOAuth ServerAuth();
|
||||
static OsmOAuth ServerAuth(std::string const & oauthToken);
|
||||
|
||||
/// master.apis.dev.openstreetmap.org
|
||||
static OsmOAuth DevServerAuth();
|
||||
/// api.openstreetmap.org
|
||||
static OsmOAuth ProductionServerAuth();
|
||||
|
||||
void SetAuthToken(std::string const & authToken);
|
||||
std::string const & GetAuthToken() const;
|
||||
bool IsAuthorized() const;
|
||||
|
||||
/// @returns false if login and/or password are invalid.
|
||||
bool AuthorizePassword(std::string const & login, std::string const & password);
|
||||
/// @returns false if Facebook credentials are invalid.
|
||||
bool AuthorizeFacebook(std::string const & facebookToken);
|
||||
/// @returns false if Google credentials are invalid.
|
||||
bool AuthorizeGoogle(std::string const & googleToken);
|
||||
/// @returns false if email has not been registered on a server.
|
||||
// bool ResetPassword(std::string const & email) const;
|
||||
|
||||
/// Throws in case of network errors.
|
||||
/// @param[method] The API method, must start with a forward slash.
|
||||
Response Request(std::string const & method, std::string const & httpMethod = "GET",
|
||||
std::string const & body = "") const;
|
||||
/// Tokenless GET request, for convenience.
|
||||
/// @param[api] If false, request is made to m_baseUrl.
|
||||
Response DirectRequest(std::string const & method, bool api = true) const;
|
||||
|
||||
// Getters
|
||||
std::string GetBaseUrl() const { return m_baseUrl; }
|
||||
std::string GetClientId() const { return m_oauth2params.m_clientId; }
|
||||
std::string GetScope() const { return m_oauth2params.m_scope; }
|
||||
std::string GetRedirectUri() const { return m_oauth2params.m_redirectUri; }
|
||||
|
||||
/// @name Methods for WebView-based authentication.
|
||||
//@{
|
||||
std::string FinishAuthorization(std::string const & oauth2code) const;
|
||||
std::string GetRegistrationURL() const { return m_baseUrl + "/user/new"; }
|
||||
std::string GetResetPasswordURL() const { return m_baseUrl + "/user/forgot-password"; }
|
||||
std::string GetHistoryURL(std::string const & user) const { return m_baseUrl + "/user/" + user + "/history"; }
|
||||
std::string GetNotesURL(std::string const & user) const { return m_baseUrl + "/user/" + user + "/notes"; }
|
||||
std::string GetDeleteURL() const { return m_baseUrl + "/account/deletion"; }
|
||||
std::string BuildOAuth2Url() const;
|
||||
//@}
|
||||
|
||||
private:
|
||||
struct SessionID
|
||||
{
|
||||
std::string m_cookies;
|
||||
std::string m_authenticityToken;
|
||||
};
|
||||
|
||||
/// OAuth2 parameters (including secret) for application.
|
||||
Oauth2Params const m_oauth2params;
|
||||
std::string const m_baseUrl;
|
||||
std::string const m_apiUrl;
|
||||
/// Token to authenticate every OAuth request.
|
||||
// std::string const m_oauth2code;
|
||||
std::string m_oauth2token;
|
||||
|
||||
SessionID FetchSessionId(std::string const & subUrl = "/login", std::string const & cookies = "") const;
|
||||
/// Log a user out.
|
||||
void LogoutUser(SessionID const & sid) const;
|
||||
/// Signs a user id using login and password.
|
||||
/// @returns false if login or password are invalid.
|
||||
bool LoginUserPassword(std::string const & login, std::string const & password, SessionID const & sid) const;
|
||||
/// Signs a user in using Facebook token.
|
||||
/// @returns false if the social token is invalid.
|
||||
bool LoginSocial(std::string const & callbackPart, std::string const & socialToken, SessionID const & sid) const;
|
||||
/// @returns non-empty string with oauth_verifier value.
|
||||
std::string SendAuthRequest(std::string const & requestTokenKey, SessionID const & lastSid) const;
|
||||
/// @returns valid key and secret or throws otherwise.
|
||||
std::string FetchRequestToken(SessionID const & sid) const;
|
||||
std::string FetchAccessToken(SessionID const & sid) const;
|
||||
};
|
||||
|
||||
std::string DebugPrint(OsmOAuth::Response const & code);
|
||||
|
||||
} // namespace osm
|
||||
10
libs/editor/osm_auth_tests/CMakeLists.txt
Normal file
10
libs/editor/osm_auth_tests/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
project(osm_auth_tests)
|
||||
|
||||
set(SRC
|
||||
osm_auth_tests.cpp
|
||||
server_api_test.cpp
|
||||
)
|
||||
|
||||
omim_add_test(${PROJECT_NAME} ${SRC})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME} editor)
|
||||
49
libs/editor/osm_auth_tests/osm_auth_tests.cpp
Normal file
49
libs/editor/osm_auth_tests/osm_auth_tests.cpp
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "editor/osm_auth.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace osm_auth
|
||||
{
|
||||
using osm::OsmOAuth;
|
||||
|
||||
char const * kValidOsmUser = "CoMapsTestUser";
|
||||
char const * kValidOsmPassword = "12345678";
|
||||
static constexpr char const * kInvalidOsmPassword = "123";
|
||||
static constexpr char const * kForgotPasswordEmail = "osmtest1@comaps.app";
|
||||
|
||||
UNIT_TEST(OSM_Auth_InvalidLogin)
|
||||
{
|
||||
OsmOAuth auth = OsmOAuth::DevServerAuth();
|
||||
bool result;
|
||||
TEST_NO_THROW(result = auth.AuthorizePassword(kValidOsmUser, kInvalidOsmPassword), ());
|
||||
TEST_EQUAL(result, false, ("invalid password"));
|
||||
TEST(!auth.IsAuthorized(), ("Should not be authorized."));
|
||||
}
|
||||
|
||||
UNIT_TEST(OSM_Auth_Login)
|
||||
{
|
||||
OsmOAuth auth = OsmOAuth::DevServerAuth();
|
||||
bool result;
|
||||
TEST_NO_THROW(result = auth.AuthorizePassword(kValidOsmUser, kValidOsmPassword), ());
|
||||
TEST_EQUAL(result, true, ("login to test server"));
|
||||
TEST(auth.IsAuthorized(), ("Should be authorized."));
|
||||
OsmOAuth::Response const perm = auth.Request("/permissions");
|
||||
TEST_EQUAL(perm.first, OsmOAuth::HTTP::OK, ("permission request ok"));
|
||||
TEST_NOT_EQUAL(perm.second.find("write_api"), std::string::npos, ("can write to api"));
|
||||
}
|
||||
|
||||
/*
|
||||
UNIT_TEST(OSM_Auth_ForgotPassword)
|
||||
{
|
||||
OsmOAuth auth = OsmOAuth::DevServerAuth();
|
||||
bool result;
|
||||
TEST_NO_THROW(result = auth.ResetPassword(kForgotPasswordEmail), ());
|
||||
TEST_EQUAL(result, true, ("Correct email"));
|
||||
TEST_NO_THROW(result = auth.ResetPassword("not@registered.email"), ());
|
||||
TEST_EQUAL(result, false, ("Incorrect email"));
|
||||
}
|
||||
*/
|
||||
} // namespace osm_auth
|
||||
144
libs/editor/osm_auth_tests/server_api_test.cpp
Normal file
144
libs/editor/osm_auth_tests/server_api_test.cpp
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "editor/server_api.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "base/scope_guard.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <random>
|
||||
#include <string>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace osm_auth
|
||||
{
|
||||
using osm::OsmOAuth;
|
||||
using osm::ServerApi06;
|
||||
using namespace pugi;
|
||||
|
||||
extern char const * kValidOsmUser;
|
||||
extern char const * kValidOsmPassword;
|
||||
|
||||
UNIT_TEST(OSM_ServerAPI_TestUserExists)
|
||||
{
|
||||
ServerApi06 api(OsmOAuth::DevServerAuth());
|
||||
// FIXME(AB): Uncomment back when HTTP 500 from https://master.apis.dev.openstreetmap.org/user/OrganicMapsTestUser
|
||||
// is fixed.
|
||||
// TEST(api.TestOSMUser(kValidOsmUser), ());
|
||||
TEST(!api.TestOSMUser("donotregisterthisuser"), ());
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
ServerApi06 CreateAPI()
|
||||
{
|
||||
OsmOAuth auth = OsmOAuth::DevServerAuth();
|
||||
bool result;
|
||||
TEST_NO_THROW(result = auth.AuthorizePassword(kValidOsmUser, kValidOsmPassword), ());
|
||||
TEST_EQUAL(result, true, ("Invalid login or password?"));
|
||||
TEST(auth.IsAuthorized(), ("OSM authorization"));
|
||||
ServerApi06 api(auth);
|
||||
// Test user preferences reading along the way.
|
||||
osm::UserPreferences prefs;
|
||||
TEST_NO_THROW(prefs = api.GetUserPreferences(), ());
|
||||
TEST_EQUAL(prefs.m_displayName, kValidOsmUser, ("User display name"));
|
||||
TEST_EQUAL(prefs.m_id, 14235, ("User id"));
|
||||
return api;
|
||||
}
|
||||
|
||||
// Returns random coordinate to avoid races when several workers run tests at the same time.
|
||||
ms::LatLon RandomCoordinate()
|
||||
{
|
||||
std::random_device rd;
|
||||
return ms::LatLon(std::uniform_real_distribution<>{-89., 89.}(rd), std::uniform_real_distribution<>{-179., 179.}(rd));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void DeleteOSMNodeIfExists(ServerApi06 const & api, uint64_t changeSetId, ms::LatLon const & ll)
|
||||
{
|
||||
// Delete all test nodes left on the server (if any).
|
||||
auto const response = api.GetXmlFeaturesAtLatLon(ll, 1.0);
|
||||
TEST_EQUAL(response.first, OsmOAuth::HTTP::OK, ());
|
||||
xml_document reply;
|
||||
reply.load_string(response.second.c_str());
|
||||
// Response can be empty, and it's ok.
|
||||
for (pugi::xml_node node : reply.child("osm").children("node"))
|
||||
{
|
||||
node.attribute("changeset") = changeSetId;
|
||||
node.remove_child("tag");
|
||||
TEST_NO_THROW(api.DeleteElement(editor::XMLFeature(node)), ());
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(OSM_ServerAPI_ChangesetAndNode)
|
||||
{
|
||||
ms::LatLon const kOriginalLocation = RandomCoordinate();
|
||||
ms::LatLon const kModifiedLocation = RandomCoordinate();
|
||||
|
||||
using editor::XMLFeature;
|
||||
XMLFeature node(XMLFeature::Type::Node);
|
||||
|
||||
ServerApi06 const api = CreateAPI();
|
||||
uint64_t changeSetId =
|
||||
api.CreateChangeSet({{"created_by", "CoMaps Unit Test"}, {"comment", "For test purposes only."}});
|
||||
auto const changesetCloser = [&]() { api.CloseChangeSet(changeSetId); };
|
||||
|
||||
{
|
||||
SCOPE_GUARD(guard, changesetCloser);
|
||||
|
||||
// Sometimes network can unexpectedly fail (or test exception can be raised), so do some cleanup before unit tests.
|
||||
DeleteOSMNodeIfExists(api, changeSetId, kOriginalLocation);
|
||||
DeleteOSMNodeIfExists(api, changeSetId, kModifiedLocation);
|
||||
|
||||
node.SetCenter(kOriginalLocation);
|
||||
node.SetAttribute("changeset", strings::to_string(changeSetId));
|
||||
node.SetAttribute("version", "1");
|
||||
node.SetTagValue("testkey", "firstnode");
|
||||
|
||||
// Pushes node to OSM server and automatically sets node id.
|
||||
api.CreateElementAndSetAttributes(node);
|
||||
TEST(!node.GetAttribute("id").empty(), ());
|
||||
|
||||
// Change node's coordinates and tags.
|
||||
node.SetCenter(kModifiedLocation);
|
||||
node.SetTagValue("testkey", "secondnode");
|
||||
api.ModifyElementAndSetVersion(node);
|
||||
// After modification, node version increases in ModifyElement.
|
||||
TEST_EQUAL(node.GetAttribute("version"), "2", ());
|
||||
|
||||
// All tags must be specified, because there is no merging of old and new tags.
|
||||
api.UpdateChangeSet(changeSetId,
|
||||
{{"created_by", "CoMaps Unit Test"}, {"comment", "For test purposes only (updated)."}});
|
||||
|
||||
// To retrieve created node, changeset should be closed first.
|
||||
// It is done here via Scope Guard.
|
||||
}
|
||||
|
||||
auto const response = api.GetXmlFeaturesAtLatLon(kModifiedLocation, 1.0);
|
||||
TEST_EQUAL(response.first, OsmOAuth::HTTP::OK, ());
|
||||
auto const features = XMLFeature::FromOSM(response.second);
|
||||
TEST_EQUAL(1, features.size(), ());
|
||||
TEST_EQUAL(node.GetAttribute("id"), features[0].GetAttribute("id"), ());
|
||||
|
||||
// Cleanup - delete unit test node from the server.
|
||||
changeSetId = api.CreateChangeSet({{"created_by", "CoMaps Unit Test"}, {"comment", "For test purposes only."}});
|
||||
SCOPE_GUARD(guard, changesetCloser);
|
||||
// New changeset has new id.
|
||||
node.SetAttribute("changeset", strings::to_string(changeSetId));
|
||||
TEST_NO_THROW(api.DeleteElement(node), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(OSM_ServerAPI_Notes)
|
||||
{
|
||||
ms::LatLon const pos = RandomCoordinate();
|
||||
ServerApi06 const api = CreateAPI();
|
||||
uint64_t id;
|
||||
TEST_NO_THROW(id = api.CreateNote(pos, "A test note"), ("Creating a note"));
|
||||
TEST_GREATER(id, 0, ("Note id should be a positive integer"));
|
||||
TEST_NO_THROW(api.CloseNote(id), ("Closing a note"));
|
||||
}
|
||||
} // namespace osm_auth
|
||||
1362
libs/editor/osm_editor.cpp
Normal file
1362
libs/editor/osm_editor.cpp
Normal file
File diff suppressed because it is too large
Load diff
271
libs/editor/osm_editor.hpp
Normal file
271
libs/editor/osm_editor.hpp
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
#pragma once
|
||||
|
||||
#include "editor/changeset_wrapper.hpp"
|
||||
#include "editor/config_loader.hpp"
|
||||
#include "editor/editor_config.hpp"
|
||||
#include "editor/editor_notes.hpp"
|
||||
#include "editor/editor_storage.hpp"
|
||||
#include "editor/new_feature_categories.hpp"
|
||||
#include "editor/xml_feature.hpp"
|
||||
|
||||
#include "indexer/edit_journal.hpp"
|
||||
#include "indexer/editable_map_object.hpp"
|
||||
#include "indexer/feature.hpp"
|
||||
#include "indexer/feature_source.hpp"
|
||||
#include "indexer/mwm_set.hpp"
|
||||
|
||||
#include "geometry/rect2d.hpp"
|
||||
|
||||
#include "base/atomic_shared_ptr.hpp"
|
||||
#include "base/thread_checker.hpp"
|
||||
#include "base/timer.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace editor::testing
|
||||
{
|
||||
class EditorTest;
|
||||
} // namespace editor::testing
|
||||
namespace editor
|
||||
{
|
||||
class XMLFeature;
|
||||
} // namespace editor
|
||||
|
||||
namespace osm
|
||||
{
|
||||
// NOTE: this class is thead-safe for read operations,
|
||||
// but write operations should be called on main thread only.
|
||||
class Editor final : public MwmSet::Observer
|
||||
{
|
||||
friend class editor::testing::EditorTest;
|
||||
|
||||
Editor();
|
||||
|
||||
public:
|
||||
using FeatureTypeFn = std::function<void(FeatureType & ft)>;
|
||||
using InvalidateFn = std::function<void()>;
|
||||
using ForEachFeaturesNearByFn = std::function<void(FeatureTypeFn && fn, m2::PointD const & mercator)>;
|
||||
using MwmId = MwmSet::MwmId;
|
||||
|
||||
struct Delegate
|
||||
{
|
||||
virtual ~Delegate() = default;
|
||||
|
||||
virtual MwmId GetMwmIdByMapName(std::string const & name) const = 0;
|
||||
virtual std::unique_ptr<EditableMapObject> GetOriginalMapObject(FeatureID const & fid) const = 0;
|
||||
virtual std::string GetOriginalFeatureStreet(FeatureID const & fid) const = 0;
|
||||
virtual void ForEachFeatureAtPoint(FeatureTypeFn && fn, m2::PointD const & point) const = 0;
|
||||
};
|
||||
|
||||
enum class UploadResult
|
||||
{
|
||||
Success,
|
||||
Error,
|
||||
NothingToUpload
|
||||
};
|
||||
|
||||
using FinishUploadCallback = std::function<void(UploadResult)>;
|
||||
|
||||
enum class SaveResult
|
||||
{
|
||||
NothingWasChanged,
|
||||
SavedSuccessfully,
|
||||
NoFreeSpaceError,
|
||||
NoUnderlyingMapError,
|
||||
SavingError
|
||||
};
|
||||
|
||||
enum class NoteProblemType
|
||||
{
|
||||
General,
|
||||
PlaceDoesNotExist
|
||||
};
|
||||
|
||||
struct Stats
|
||||
{
|
||||
/// <id, feature status string>
|
||||
std::vector<std::pair<FeatureID, std::string>> m_edits;
|
||||
size_t m_uploadedCount = 0;
|
||||
time_t m_lastUploadTimestamp = base::INVALID_TIME_STAMP;
|
||||
};
|
||||
|
||||
static Editor & Instance();
|
||||
|
||||
void SetDelegate(std::unique_ptr<Delegate> delegate) { m_delegate = std::move(delegate); }
|
||||
|
||||
void SetStorageForTesting(std::unique_ptr<editor::StorageBase> storage) { m_storage = std::move(storage); }
|
||||
|
||||
void ResetNotes() { m_notes = editor::Notes::MakeNotes(); }
|
||||
|
||||
void SetDefaultStorage();
|
||||
|
||||
void SetInvalidateFn(InvalidateFn const & fn) { m_invalidateFn = fn; }
|
||||
|
||||
void LoadEdits();
|
||||
/// Resets editor to initial state: no any edits or created/deleted features.
|
||||
void ClearAllLocalEdits();
|
||||
|
||||
// MwmSet::Observer overrides:
|
||||
void OnMapRegistered(platform::LocalCountryFile const & localFile) override;
|
||||
|
||||
using FeatureIndexFunctor = std::function<void(uint32_t)>;
|
||||
void ForEachCreatedFeature(MwmId const & id, FeatureIndexFunctor const & f, m2::RectD const & rect, int scale) const;
|
||||
|
||||
/// Easy way to check if a feature was deleted, modified, created or not changed at all.
|
||||
FeatureStatus GetFeatureStatus(MwmId const & mwmId, uint32_t index) const;
|
||||
FeatureStatus GetFeatureStatus(FeatureID const & fid) const;
|
||||
|
||||
/// @returns true if a feature was uploaded to osm.
|
||||
bool IsFeatureUploaded(MwmId const & mwmId, uint32_t index) const;
|
||||
|
||||
/// Marks feature as "deleted" from MwM file.
|
||||
void DeleteFeature(FeatureID const & fid);
|
||||
|
||||
/// @returns empty object if feature wasn't edited.
|
||||
std::optional<osm::EditableMapObject> GetEditedFeature(FeatureID const & fid) const;
|
||||
|
||||
/// @returns empty object if feature wasn't edited.
|
||||
std::optional<osm::EditJournal> GetEditedFeatureJournal(FeatureID const & fid) const;
|
||||
|
||||
/// @returns false if feature wasn't edited.
|
||||
/// @param outFeatureStreet is valid only if true was returned.
|
||||
bool GetEditedFeatureStreet(FeatureID const & fid, std::string & outFeatureStreet) const;
|
||||
|
||||
/// @returns sorted features indices with specified status.
|
||||
std::vector<uint32_t> GetFeaturesByStatus(MwmId const & mwmId, FeatureStatus status) const;
|
||||
|
||||
/// Editor checks internally if any feature params were actually edited.
|
||||
SaveResult SaveEditedFeature(EditableMapObject const & emo);
|
||||
|
||||
/// Removes changes from editor.
|
||||
/// @returns false if a feature was uploaded.
|
||||
bool RollBackChanges(FeatureID const & fid);
|
||||
|
||||
EditableProperties GetEditableProperties(FeatureType & feature) const;
|
||||
|
||||
bool HaveMapEditsOrNotesToUpload() const;
|
||||
bool HaveMapEditsToUpload(MwmId const & mwmId) const;
|
||||
|
||||
using ChangesetTags = std::map<std::string, std::string>;
|
||||
/// Tries to upload all local changes to OSM server in a separate thread.
|
||||
/// @param[in] tags should provide additional information about client to use in changeset.
|
||||
void UploadChanges(std::string const & oauthToken, ChangesetTags tags,
|
||||
FinishUploadCallback callBack = FinishUploadCallback());
|
||||
// TODO(mgsergio): Test new types from new config but with old classificator (where these types are absent).
|
||||
// Editor should silently ignore all types in config which are unknown to him.
|
||||
NewFeatureCategories GetNewFeatureCategories() const;
|
||||
|
||||
bool CreatePoint(uint32_t type, m2::PointD const & mercator, MwmId const & id, EditableMapObject & outFeature) const;
|
||||
|
||||
void CreateNote(ms::LatLon const & latLon, FeatureID const & fid, feature::TypesHolder const & holder,
|
||||
std::string_view defaultName, NoteProblemType type, std::string_view note);
|
||||
|
||||
Stats GetStats() const;
|
||||
|
||||
void CreateStandaloneNote(ms::LatLon const & latLon, std::string const & noteText);
|
||||
|
||||
// Don't use this function to determine if a feature in editor was created.
|
||||
// Use GetFeatureStatus(fid) instead. This function is used when a feature is
|
||||
// not yet saved, and we have to know if it was modified or created.
|
||||
static bool IsCreatedFeature(FeatureID const & fid);
|
||||
|
||||
private:
|
||||
// TODO(a): Use this structure as part of FeatureTypeInfo.
|
||||
struct UploadInfo
|
||||
{
|
||||
time_t m_uploadAttemptTimestamp = base::INVALID_TIME_STAMP;
|
||||
/// Is empty if upload has never occurred or one of k* constants above otherwise.
|
||||
std::string m_uploadStatus;
|
||||
std::string m_uploadError;
|
||||
};
|
||||
|
||||
struct FeatureTypeInfo
|
||||
{
|
||||
FeatureStatus m_status = FeatureStatus::Untouched;
|
||||
EditableMapObject m_object;
|
||||
/// If not empty contains Feature's addr:street, edited by user.
|
||||
std::string m_street;
|
||||
time_t m_modificationTimestamp = base::INVALID_TIME_STAMP;
|
||||
time_t m_uploadAttemptTimestamp = base::INVALID_TIME_STAMP;
|
||||
/// Is empty if upload has never occurred or one of k* constants above otherwise.
|
||||
std::string m_uploadStatus;
|
||||
std::string m_uploadError;
|
||||
};
|
||||
|
||||
using FeaturesContainer = std::map<MwmId, std::map<uint32_t, FeatureTypeInfo>>;
|
||||
|
||||
/// @returns false if fails.
|
||||
bool Save(FeaturesContainer const & features) const;
|
||||
bool SaveTransaction(std::shared_ptr<FeaturesContainer> const & features);
|
||||
bool RemoveFeatureIfExists(FeatureID const & fid);
|
||||
/// Notify framework that something has changed and should be redisplayed.
|
||||
void Invalidate();
|
||||
|
||||
// Saves a feature in internal storage with FeatureStatus::Obsolete status.
|
||||
bool MarkFeatureAsObsolete(FeatureID const & fid);
|
||||
bool RemoveFeature(FeatureID const & fid);
|
||||
|
||||
FeatureID GenerateNewFeatureId(FeaturesContainer const & features, MwmId const & id) const;
|
||||
EditableProperties GetEditablePropertiesForTypes(feature::TypesHolder const & types) const;
|
||||
|
||||
bool FillFeatureInfo(FeatureStatus status, editor::XMLFeature const & xml, FeatureID const & fid,
|
||||
FeatureTypeInfo & fti) const;
|
||||
/// @returns pointer to m_features[id][index] if exists, nullptr otherwise.
|
||||
static FeatureTypeInfo const * GetFeatureTypeInfo(FeaturesContainer const & features, MwmId const & mwmId,
|
||||
uint32_t index);
|
||||
void SaveUploadedInformation(FeatureID const & fid, UploadInfo const & fromUploader);
|
||||
|
||||
void MarkFeatureWithStatus(FeaturesContainer & editableFeatures, FeatureID const & fid, FeatureStatus status);
|
||||
|
||||
// These methods are just checked wrappers around Delegate.
|
||||
MwmId GetMwmIdByMapName(std::string const & name);
|
||||
std::unique_ptr<EditableMapObject> GetOriginalMapObject(FeatureID const & fid) const;
|
||||
std::string GetOriginalFeatureStreet(FeatureID const & fid) const;
|
||||
void ForEachFeatureAtPoint(FeatureTypeFn && fn, m2::PointD const & point) const;
|
||||
FeatureID GetFeatureIdByXmlFeature(FeaturesContainer const & features, editor::XMLFeature const & xml,
|
||||
MwmId const & mwmId, FeatureStatus status, bool needMigrate) const;
|
||||
void LoadMwmEdits(FeaturesContainer & loadedFeatures, pugi::xml_node const & mwm, MwmId const & mwmId,
|
||||
bool needMigrate);
|
||||
|
||||
static bool HaveMapEditsToUpload(FeaturesContainer const & features);
|
||||
|
||||
static FeatureStatus GetFeatureStatusImpl(FeaturesContainer const & features, MwmId const & mwmId, uint32_t index);
|
||||
|
||||
static bool IsFeatureUploadedImpl(FeaturesContainer const & features, MwmId const & mwmId, uint32_t index);
|
||||
|
||||
static void UpdateXMLFeatureTags(editor::XMLFeature & feature, std::list<JournalEntry> const & journal,
|
||||
ChangesetWrapper & changeset);
|
||||
|
||||
/// Deleted, edited and created features.
|
||||
base::AtomicSharedPtr<FeaturesContainer> m_features;
|
||||
|
||||
std::unique_ptr<Delegate> m_delegate;
|
||||
|
||||
/// Invalidate map viewport after edits.
|
||||
InvalidateFn m_invalidateFn;
|
||||
|
||||
/// Contains information about what and how can be edited.
|
||||
base::AtomicSharedPtr<editor::EditorConfig> m_config;
|
||||
editor::ConfigLoader m_configLoader;
|
||||
|
||||
/// Notes to be sent to osm.
|
||||
std::shared_ptr<editor::Notes> m_notes;
|
||||
|
||||
std::unique_ptr<editor::StorageBase> m_storage;
|
||||
|
||||
std::mutex m_uploadingEditsMutex;
|
||||
|
||||
DECLARE_THREAD_CHECKER(MainThreadChecker);
|
||||
}; // class Editor
|
||||
|
||||
std::string DebugPrint(Editor::SaveResult saveResult);
|
||||
} // namespace osm
|
||||
212
libs/editor/server_api.cpp
Normal file
212
libs/editor/server_api.cpp
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
#include "editor/server_api.hpp"
|
||||
|
||||
#include "coding/url.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
#include "base/math.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
#include "base/timer.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string KeyValueTagsToXML(osm::ServerApi06::KeyValueTags const & kvTags)
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << "<osm>\n"
|
||||
"<changeset>\n";
|
||||
for (auto const & tag : kvTags)
|
||||
stream << " <tag k=\"" << tag.first << "\" v=\"" << tag.second << "\"/>\n";
|
||||
stream << "</changeset>\n"
|
||||
"</osm>\n";
|
||||
return stream.str();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace osm
|
||||
{
|
||||
|
||||
ServerApi06::ServerApi06(OsmOAuth const & auth) : m_auth(auth) {}
|
||||
|
||||
uint64_t ServerApi06::CreateChangeSet(KeyValueTags const & kvTags) const
|
||||
{
|
||||
if (!m_auth.IsAuthorized())
|
||||
MYTHROW(NotAuthorized, ("Not authorized."));
|
||||
|
||||
OsmOAuth::Response const response = m_auth.Request("/changeset/create", "PUT", KeyValueTagsToXML(kvTags));
|
||||
if (response.first != OsmOAuth::HTTP::OK)
|
||||
MYTHROW(CreateChangeSetHasFailed, ("CreateChangeSet request has failed:", response));
|
||||
|
||||
uint64_t id;
|
||||
if (!strings::to_uint64(response.second, id))
|
||||
MYTHROW(CantParseServerResponse, ("Can't parse changeset ID from server response."));
|
||||
return id;
|
||||
}
|
||||
|
||||
uint64_t ServerApi06::CreateElement(editor::XMLFeature const & element) const
|
||||
{
|
||||
OsmOAuth::Response const response =
|
||||
m_auth.Request("/" + element.GetTypeString() + "/create", "PUT", element.ToOSMString());
|
||||
if (response.first != OsmOAuth::HTTP::OK)
|
||||
MYTHROW(CreateElementHasFailed, ("CreateElement request has failed:", response, "for", element));
|
||||
uint64_t id;
|
||||
if (!strings::to_uint64(response.second, id))
|
||||
MYTHROW(CantParseServerResponse, ("Can't parse created node ID from server response."));
|
||||
return id;
|
||||
}
|
||||
|
||||
void ServerApi06::CreateElementAndSetAttributes(editor::XMLFeature & element) const
|
||||
{
|
||||
uint64_t const id = CreateElement(element);
|
||||
element.SetAttribute("id", strings::to_string(id));
|
||||
element.SetAttribute("version", "1");
|
||||
}
|
||||
|
||||
uint64_t ServerApi06::ModifyElement(editor::XMLFeature const & element) const
|
||||
{
|
||||
std::string const id = element.GetAttribute("id");
|
||||
CHECK(!id.empty(), ("id attribute is missing for", element));
|
||||
|
||||
OsmOAuth::Response const response =
|
||||
m_auth.Request("/" + element.GetTypeString() + "/" + id, "PUT", element.ToOSMString());
|
||||
if (response.first != OsmOAuth::HTTP::OK)
|
||||
MYTHROW(ModifyElementHasFailed, ("ModifyElement request has failed:", response, "for", element));
|
||||
uint64_t version;
|
||||
if (!strings::to_uint64(response.second, version))
|
||||
MYTHROW(CantParseServerResponse, ("Can't parse element version from server response", response.second));
|
||||
return version;
|
||||
}
|
||||
|
||||
void ServerApi06::ModifyElementAndSetVersion(editor::XMLFeature & element) const
|
||||
{
|
||||
uint64_t const version = ModifyElement(element);
|
||||
element.SetAttribute("version", strings::to_string(version));
|
||||
}
|
||||
|
||||
void ServerApi06::DeleteElement(editor::XMLFeature const & element) const
|
||||
{
|
||||
std::string const id = element.GetAttribute("id");
|
||||
if (id.empty())
|
||||
MYTHROW(DeletedElementHasNoIdAttribute, ("Please set id attribute for", element));
|
||||
|
||||
OsmOAuth::Response const response =
|
||||
m_auth.Request("/" + element.GetTypeString() + "/" + id, "DELETE", element.ToOSMString());
|
||||
if (response.first != OsmOAuth::HTTP::OK && response.first != OsmOAuth::HTTP::Gone)
|
||||
MYTHROW(ErrorDeletingElement, ("Could not delete an element:", response));
|
||||
}
|
||||
|
||||
void ServerApi06::UpdateChangeSet(uint64_t changesetId, KeyValueTags const & kvTags) const
|
||||
{
|
||||
OsmOAuth::Response const response =
|
||||
m_auth.Request("/changeset/" + strings::to_string(changesetId), "PUT", KeyValueTagsToXML(kvTags));
|
||||
if (response.first != OsmOAuth::HTTP::OK)
|
||||
MYTHROW(UpdateChangeSetHasFailed, ("UpdateChangeSet request has failed:", response));
|
||||
}
|
||||
|
||||
void ServerApi06::CloseChangeSet(uint64_t changesetId) const
|
||||
{
|
||||
OsmOAuth::Response const response = m_auth.Request("/changeset/" + strings::to_string(changesetId) + "/close", "PUT");
|
||||
if (response.first != OsmOAuth::HTTP::OK)
|
||||
MYTHROW(ErrorClosingChangeSet, ("CloseChangeSet request has failed:", response));
|
||||
}
|
||||
|
||||
uint64_t ServerApi06::CreateNote(ms::LatLon const & ll, std::string const & message) const
|
||||
{
|
||||
CHECK(!message.empty(), ("Note content should not be empty."));
|
||||
std::string const params = "?lat=" + strings::to_string_dac(ll.m_lat, 7) +
|
||||
"&lon=" + strings::to_string_dac(ll.m_lon, 7) +
|
||||
"&text=" + url::UrlEncode(message + " #CoMaps " + OMIM_OS_NAME + " " + GetPlatform().Version());
|
||||
OsmOAuth::Response const response = m_auth.Request("/notes" + params, "POST");
|
||||
if (response.first != OsmOAuth::HTTP::OK)
|
||||
MYTHROW(ErrorAddingNote, ("Could not post a new note:", response));
|
||||
pugi::xml_document details;
|
||||
if (!details.load_string(response.second.c_str()))
|
||||
MYTHROW(CantParseServerResponse, ("Could not parse a note XML response", response));
|
||||
pugi::xml_node const uid = details.child("osm").child("note").child("id");
|
||||
if (!uid)
|
||||
MYTHROW(CantParseServerResponse, ("Caould not find a note id", response));
|
||||
return uid.text().as_ullong();
|
||||
}
|
||||
|
||||
void ServerApi06::CloseNote(uint64_t const id) const
|
||||
{
|
||||
OsmOAuth::Response const response = m_auth.Request("/notes/" + strings::to_string(id) + "/close", "POST");
|
||||
if (response.first != OsmOAuth::HTTP::OK)
|
||||
MYTHROW(ErrorDeletingElement, ("Could not close a note:", response));
|
||||
}
|
||||
|
||||
bool ServerApi06::TestOSMUser(std::string const & userName)
|
||||
{
|
||||
std::string const method = "/user/" + url::UrlEncode(userName);
|
||||
return m_auth.DirectRequest(method, false).first == OsmOAuth::HTTP::OK;
|
||||
}
|
||||
|
||||
UserPreferences ServerApi06::GetUserPreferences() const
|
||||
{
|
||||
try
|
||||
{
|
||||
OsmOAuth::Response const response = m_auth.Request("/user/details");
|
||||
if (response.first != OsmOAuth::HTTP::OK)
|
||||
MYTHROW(CantGetUserPreferences, (response));
|
||||
|
||||
pugi::xml_document details;
|
||||
if (!details.load_string(response.second.c_str()))
|
||||
MYTHROW(CantParseUserPreferences, (response));
|
||||
|
||||
pugi::xml_node const user = details.child("osm").child("user");
|
||||
if (!user || !user.attribute("id"))
|
||||
MYTHROW(CantParseUserPreferences, ("No <user> or 'id' attribute", response));
|
||||
|
||||
UserPreferences pref;
|
||||
pref.m_id = user.attribute("id").as_ullong();
|
||||
pref.m_displayName = user.attribute("display_name").as_string();
|
||||
pref.m_accountCreated = base::StringToTimestamp(user.attribute("account_created").as_string());
|
||||
pref.m_imageUrl = user.child("img").attribute("href").as_string();
|
||||
pref.m_changesets = user.child("changesets").attribute("count").as_uint();
|
||||
return pref;
|
||||
}
|
||||
catch (std::exception const & e)
|
||||
{
|
||||
LOG(LWARNING, ("Can't load user preferences from server: ", e.what()));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
OsmOAuth::Response ServerApi06::GetXmlFeaturesInRect(double minLat, double minLon, double maxLat, double maxLon) const
|
||||
{
|
||||
using strings::to_string_dac;
|
||||
|
||||
// Digits After Comma.
|
||||
static constexpr double kDAC = 7;
|
||||
std::string const url = "/map?bbox=" + to_string_dac(minLon, kDAC) + ',' + to_string_dac(minLat, kDAC) + ',' +
|
||||
to_string_dac(maxLon, kDAC) + ',' + to_string_dac(maxLat, kDAC);
|
||||
|
||||
return m_auth.DirectRequest(url);
|
||||
}
|
||||
|
||||
OsmOAuth::Response ServerApi06::GetXmlFeaturesAtLatLon(double lat, double lon, double radiusInMeters) const
|
||||
{
|
||||
double const latDegreeOffset = radiusInMeters * mercator::Bounds::kDegreesInMeter;
|
||||
double const minLat = std::max(-90.0, lat - latDegreeOffset);
|
||||
double const maxLat = std::min(90.0, lat + latDegreeOffset);
|
||||
double const cosL = std::max(cos(math::DegToRad(std::max(fabs(minLat), fabs(maxLat)))), 0.00001);
|
||||
double const lonDegreeOffset = radiusInMeters * mercator::Bounds::kDegreesInMeter / cosL;
|
||||
double const minLon = std::max(-180.0, lon - lonDegreeOffset);
|
||||
double const maxLon = std::min(180.0, lon + lonDegreeOffset);
|
||||
return GetXmlFeaturesInRect(minLat, minLon, maxLat, maxLon);
|
||||
}
|
||||
|
||||
OsmOAuth::Response ServerApi06::GetXmlFeaturesAtLatLon(ms::LatLon const & ll, double radiusInMeters) const
|
||||
{
|
||||
return GetXmlFeaturesAtLatLon(ll.m_lat, ll.m_lon, radiusInMeters);
|
||||
}
|
||||
|
||||
} // namespace osm
|
||||
87
libs/editor/server_api.hpp
Normal file
87
libs/editor/server_api.hpp
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#pragma once
|
||||
|
||||
#include "editor/osm_auth.hpp"
|
||||
#include "editor/xml_feature.hpp"
|
||||
|
||||
#include "geometry/latlon.hpp"
|
||||
#include "geometry/rect2d.hpp"
|
||||
|
||||
#include "base/exception.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace osm
|
||||
{
|
||||
struct UserPreferences
|
||||
{
|
||||
uint64_t m_id;
|
||||
std::string m_displayName;
|
||||
time_t m_accountCreated;
|
||||
std::string m_imageUrl;
|
||||
uint32_t m_changesets;
|
||||
};
|
||||
|
||||
/// All methods here are synchronous and need wrappers for async usage.
|
||||
/// Exceptions are used for error handling.
|
||||
class ServerApi06
|
||||
{
|
||||
public:
|
||||
// k= and v= tags used in OSM.
|
||||
using KeyValueTags = std::map<std::string, std::string>;
|
||||
|
||||
DECLARE_EXCEPTION(ServerApi06Exception, RootException);
|
||||
DECLARE_EXCEPTION(NotAuthorized, ServerApi06Exception);
|
||||
DECLARE_EXCEPTION(CantParseServerResponse, ServerApi06Exception);
|
||||
DECLARE_EXCEPTION(CreateChangeSetHasFailed, ServerApi06Exception);
|
||||
DECLARE_EXCEPTION(UpdateChangeSetHasFailed, ServerApi06Exception);
|
||||
DECLARE_EXCEPTION(CreateElementHasFailed, ServerApi06Exception);
|
||||
DECLARE_EXCEPTION(ModifyElementHasFailed, ServerApi06Exception);
|
||||
DECLARE_EXCEPTION(ErrorClosingChangeSet, ServerApi06Exception);
|
||||
DECLARE_EXCEPTION(ErrorAddingNote, ServerApi06Exception);
|
||||
DECLARE_EXCEPTION(DeletedElementHasNoIdAttribute, ServerApi06Exception);
|
||||
DECLARE_EXCEPTION(ErrorDeletingElement, ServerApi06Exception);
|
||||
DECLARE_EXCEPTION(CantGetUserPreferences, ServerApi06Exception);
|
||||
DECLARE_EXCEPTION(CantParseUserPreferences, ServerApi06Exception);
|
||||
|
||||
ServerApi06(OsmOAuth const & auth);
|
||||
/// This function can be used to check if user did not confirm email validation link after registration.
|
||||
/// Throws if there is no connection.
|
||||
/// @returns true if user have registered/signed up even if his email address was not confirmed yet.
|
||||
bool TestOSMUser(std::string const & userName);
|
||||
/// Get OSM user preferences in a convenient struct.
|
||||
UserPreferences GetUserPreferences() const;
|
||||
/// Please use at least created_by=* and comment=* tags.
|
||||
/// @returns created changeset ID.
|
||||
uint64_t CreateChangeSet(KeyValueTags const & kvTags) const;
|
||||
/// <node>, <way> or <relation> are supported.
|
||||
/// Only one element per call is supported.
|
||||
/// @returns id of created element.
|
||||
uint64_t CreateElement(editor::XMLFeature const & element) const;
|
||||
/// The same as const version but also updates id and version for passed element.
|
||||
void CreateElementAndSetAttributes(editor::XMLFeature & element) const;
|
||||
/// @param element should already have all attributes set, including "id", "version", "changeset".
|
||||
/// @returns new version of modified element.
|
||||
uint64_t ModifyElement(editor::XMLFeature const & element) const;
|
||||
/// Sets element's version.
|
||||
void ModifyElementAndSetVersion(editor::XMLFeature & element) const;
|
||||
/// Some nodes can't be deleted if they are used in ways or relations.
|
||||
/// @param element should already have all attributes set, including "id", "version", "changeset".
|
||||
/// @returns true if element was successfully deleted (or was already deleted).
|
||||
void DeleteElement(editor::XMLFeature const & element) const;
|
||||
void UpdateChangeSet(uint64_t changesetId, KeyValueTags const & kvTags) const;
|
||||
void CloseChangeSet(uint64_t changesetId) const;
|
||||
/// @returns id of a created note.
|
||||
uint64_t CreateNote(ms::LatLon const & ll, std::string const & message) const;
|
||||
void CloseNote(uint64_t const id) const;
|
||||
|
||||
/// @returns OSM xml string with features in the bounding box or empty string on error.
|
||||
OsmOAuth::Response GetXmlFeaturesInRect(double minLat, double minLon, double maxLat, double maxLon) const;
|
||||
OsmOAuth::Response GetXmlFeaturesAtLatLon(double lat, double lon, double radiusInMeters = 1.0) const;
|
||||
OsmOAuth::Response GetXmlFeaturesAtLatLon(ms::LatLon const & ll, double radiusInMeters = 1.0) const;
|
||||
|
||||
private:
|
||||
OsmOAuth m_auth;
|
||||
};
|
||||
|
||||
} // namespace osm
|
||||
380
libs/editor/ui2oh.cpp
Normal file
380
libs/editor/ui2oh.cpp
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
#include "editor/ui2oh.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "3party/opening_hours/opening_hours.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using osmoh::operator""_h;
|
||||
|
||||
osmoh::Timespan const kTwentyFourHours = {0_h, 24_h};
|
||||
|
||||
editor::ui::OpeningDays MakeOpeningDays(osmoh::Weekdays const & wds)
|
||||
{
|
||||
std::set<osmoh::Weekday> openingDays;
|
||||
for (auto const & wd : wds.GetWeekdayRanges())
|
||||
{
|
||||
if (wd.HasSunday())
|
||||
openingDays.insert(osmoh::Weekday::Sunday);
|
||||
if (wd.HasMonday())
|
||||
openingDays.insert(osmoh::Weekday::Monday);
|
||||
if (wd.HasTuesday())
|
||||
openingDays.insert(osmoh::Weekday::Tuesday);
|
||||
if (wd.HasWednesday())
|
||||
openingDays.insert(osmoh::Weekday::Wednesday);
|
||||
if (wd.HasThursday())
|
||||
openingDays.insert(osmoh::Weekday::Thursday);
|
||||
if (wd.HasFriday())
|
||||
openingDays.insert(osmoh::Weekday::Friday);
|
||||
if (wd.HasSaturday())
|
||||
openingDays.insert(osmoh::Weekday::Saturday);
|
||||
}
|
||||
return openingDays;
|
||||
}
|
||||
|
||||
void SetUpWeekdays(osmoh::Weekdays const & wds, editor::ui::TimeTable & tt)
|
||||
{
|
||||
tt.SetOpeningDays(MakeOpeningDays(wds));
|
||||
}
|
||||
|
||||
void SetUpTimeTable(osmoh::TTimespans spans, editor::ui::TimeTable & tt)
|
||||
{
|
||||
using namespace osmoh;
|
||||
|
||||
// Expand plus: 13:15+ -> 13:15-24:00.
|
||||
for (auto & span : spans)
|
||||
span.ExpandPlus();
|
||||
|
||||
std::sort(std::begin(spans), std::end(spans), [](Timespan const & a, Timespan const & b)
|
||||
{
|
||||
auto const start1 = a.GetStart().GetHourMinutes().GetDuration();
|
||||
auto const start2 = b.GetStart().GetHourMinutes().GetDuration();
|
||||
|
||||
return start1 < start2;
|
||||
});
|
||||
|
||||
// Take first start and last end as opening time span.
|
||||
tt.SetOpeningTime({spans.front().GetStart(), spans.back().GetEnd()});
|
||||
|
||||
// Add an end of a span of index i and start of following span
|
||||
// as exclude time.
|
||||
for (size_t i = 0; i + 1 < spans.size(); ++i)
|
||||
tt.AddExcludeTime({spans[i].GetEnd(), spans[i + 1].GetStart()});
|
||||
}
|
||||
|
||||
int32_t WeekdayNumber(osmoh::Weekday const wd)
|
||||
{
|
||||
return static_cast<int32_t>(wd);
|
||||
}
|
||||
|
||||
constexpr uint32_t kDaysInWeek = 7;
|
||||
|
||||
// Shifts values from 1 to 7 like this: 1 2 3 4 5 6 7 -> 2 3 4 5 6 7 1
|
||||
int32_t NextWeekdayNumber(osmoh::Weekday const wd)
|
||||
{
|
||||
auto dayNumber = WeekdayNumber(wd);
|
||||
// If first element of the gourp would be evaluated to 0
|
||||
// the resulting formula whould be (dayNumber + 1) % kDaysInWeek.
|
||||
// Since the first one evaluates to 1
|
||||
// the formula is:
|
||||
return dayNumber % kDaysInWeek + 1;
|
||||
}
|
||||
|
||||
// Returns a vector of Weekdays with no gaps and with Sunday as the last day.
|
||||
// Exampls:
|
||||
// su, mo, we -> mo, we, su;
|
||||
// su, mo, fr, sa -> fr, sa, su, mo.
|
||||
std::vector<osmoh::Weekday> RemoveInversion(editor::ui::OpeningDays const & days)
|
||||
{
|
||||
std::vector<osmoh::Weekday> result(begin(days), end(days));
|
||||
if ((NextWeekdayNumber(result.back()) != WeekdayNumber(result.front()) && result.back() != osmoh::Weekday::Sunday) ||
|
||||
result.size() < 2)
|
||||
return result;
|
||||
|
||||
auto inversion = adjacent_find(begin(result), end(result), [](osmoh::Weekday const a, osmoh::Weekday const b)
|
||||
{ return NextWeekdayNumber(a) != WeekdayNumber(b); });
|
||||
|
||||
if (inversion != end(result))
|
||||
rotate(begin(result), ++inversion, end(result));
|
||||
|
||||
if (result.front() == osmoh::Weekday::Sunday)
|
||||
rotate(begin(result), begin(result) + 1, end(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
using Weekdays = std::vector<osmoh::Weekday>;
|
||||
|
||||
std::vector<Weekdays> SplitIntoIntervals(editor::ui::OpeningDays const & days)
|
||||
{
|
||||
ASSERT_GREATER(days.size(), 0, ("At least one day must present."));
|
||||
std::vector<Weekdays> result;
|
||||
auto const & noInversionDays = RemoveInversion(days);
|
||||
ASSERT(!noInversionDays.empty(), ());
|
||||
|
||||
auto previous = *begin(noInversionDays);
|
||||
result.push_back({previous});
|
||||
|
||||
for (auto it = next(begin(noInversionDays)); it != end(noInversionDays); ++it)
|
||||
{
|
||||
if (NextWeekdayNumber(previous) != WeekdayNumber(*it))
|
||||
result.push_back({});
|
||||
result.back().push_back(*it);
|
||||
previous = *it;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
osmoh::Weekdays MakeWeekdays(editor::ui::TimeTable const & tt)
|
||||
{
|
||||
osmoh::Weekdays wds;
|
||||
|
||||
for (auto const & daysInterval : SplitIntoIntervals(tt.GetOpeningDays()))
|
||||
{
|
||||
osmoh::WeekdayRange wdr;
|
||||
wdr.SetStart(*begin(daysInterval));
|
||||
if (daysInterval.size() > 1)
|
||||
wdr.SetEnd(*(prev(end(daysInterval))));
|
||||
|
||||
wds.AddWeekdayRange(wdr);
|
||||
}
|
||||
|
||||
return wds;
|
||||
}
|
||||
|
||||
osmoh::TTimespans MakeTimespans(editor::ui::TimeTable const & tt)
|
||||
{
|
||||
if (tt.IsTwentyFourHours())
|
||||
return {kTwentyFourHours};
|
||||
|
||||
auto const & excludeTime = tt.GetExcludeTime();
|
||||
if (excludeTime.empty())
|
||||
return {tt.GetOpeningTime()};
|
||||
|
||||
osmoh::TTimespans spans{{tt.GetOpeningTime().GetStart(), excludeTime[0].GetStart()}};
|
||||
|
||||
for (size_t i = 0; i + 1 < excludeTime.size(); ++i)
|
||||
spans.emplace_back(excludeTime[i].GetEnd(), excludeTime[i + 1].GetStart());
|
||||
|
||||
spans.emplace_back(excludeTime.back().GetEnd(), tt.GetOpeningTime().GetEnd());
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
editor::ui::OpeningDays const kWholeWeek = {
|
||||
osmoh::Weekday::Monday, osmoh::Weekday::Tuesday, osmoh::Weekday::Wednesday, osmoh::Weekday::Thursday,
|
||||
osmoh::Weekday::Friday, osmoh::Weekday::Saturday, osmoh::Weekday::Sunday};
|
||||
|
||||
editor::ui::OpeningDays GetCommonDays(editor::ui::OpeningDays const & a, editor::ui::OpeningDays const & b)
|
||||
{
|
||||
editor::ui::OpeningDays result;
|
||||
std::set_intersection(begin(a), end(a), begin(b), end(b), inserter(result, begin(result)));
|
||||
return result;
|
||||
}
|
||||
|
||||
osmoh::HourMinutes::TMinutes::rep GetDuration(osmoh::Time const & time)
|
||||
{
|
||||
return time.GetHourMinutes().GetDurationCount();
|
||||
}
|
||||
|
||||
bool Includes(osmoh::Timespan const & a, osmoh::Timespan const & b)
|
||||
{
|
||||
return GetDuration(a.GetStart()) <= GetDuration(b.GetStart()) && GetDuration(b.GetEnd()) <= GetDuration(a.GetEnd());
|
||||
}
|
||||
|
||||
bool ExcludeRulePart(osmoh::RuleSequence const & rulePart, editor::ui::TimeTableSet & tts)
|
||||
{
|
||||
auto const ttsInitialSize = tts.Size();
|
||||
for (size_t i = 0; i < ttsInitialSize; ++i)
|
||||
{
|
||||
auto tt = tts.Get(i);
|
||||
auto const ttOpeningDays = tt.GetOpeningDays();
|
||||
auto const commonDays = GetCommonDays(ttOpeningDays, MakeOpeningDays(rulePart.GetWeekdays()));
|
||||
|
||||
auto const removeCommonDays = [&commonDays](editor::ui::TimeTableSet::Proxy & tt)
|
||||
{
|
||||
for (auto const day : commonDays)
|
||||
VERIFY(tt.RemoveWorkingDay(day), ("Can't remove working day"));
|
||||
VERIFY(tt.Commit(), ("Can't commit changes"));
|
||||
};
|
||||
|
||||
auto const twentyFourHoursGuard = [](editor::ui::TimeTable & tt)
|
||||
{
|
||||
if (tt.IsTwentyFourHours())
|
||||
{
|
||||
tt.SetTwentyFourHours(false);
|
||||
// TODO(mgsergio): Consider TimeTable refactoring:
|
||||
// get rid of separation of TwentyFourHours and OpeningTime.
|
||||
tt.SetOpeningTime(kTwentyFourHours);
|
||||
}
|
||||
};
|
||||
|
||||
auto const & excludeTime = rulePart.GetTimes();
|
||||
// The whole rule matches to the tt.
|
||||
if (commonDays.size() == ttOpeningDays.size())
|
||||
{
|
||||
// rulePart applies to commonDays in a whole.
|
||||
if (excludeTime.empty())
|
||||
return tts.Remove(i);
|
||||
|
||||
twentyFourHoursGuard(tt);
|
||||
|
||||
for (auto const & time : excludeTime)
|
||||
{
|
||||
// Whatever it is, it's already closed at a time out of opening time.
|
||||
if (!Includes(tt.GetOpeningTime(), time))
|
||||
continue;
|
||||
|
||||
// The whole opening time interval should be switched off
|
||||
if (!tt.AddExcludeTime(time))
|
||||
return tts.Remove(i);
|
||||
}
|
||||
VERIFY(tt.Commit(), ("Can't update time table"));
|
||||
return true;
|
||||
}
|
||||
// A rule is applied to a subset of a time table. We should
|
||||
// subtract common parts from tt and add a new time table if needed.
|
||||
if (commonDays.size() != 0)
|
||||
{
|
||||
// rulePart applies to commonDays in a whole.
|
||||
if (excludeTime.empty())
|
||||
{
|
||||
removeCommonDays(tt);
|
||||
continue;
|
||||
}
|
||||
|
||||
twentyFourHoursGuard(tt);
|
||||
|
||||
editor::ui::TimeTable copy = tt;
|
||||
VERIFY(copy.SetOpeningDays(commonDays), ("Can't set opening days"));
|
||||
|
||||
auto doAppendRest = true;
|
||||
for (auto const & time : excludeTime)
|
||||
{
|
||||
// Whatever it is, it's already closed at a time out of opening time.
|
||||
if (!Includes(copy.GetOpeningTime(), time))
|
||||
continue;
|
||||
|
||||
// The whole opening time interval should be switched off
|
||||
if (!copy.AddExcludeTime(time))
|
||||
{
|
||||
doAppendRest = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
removeCommonDays(tt);
|
||||
|
||||
if (doAppendRest)
|
||||
VERIFY(tts.Append(copy), ("Can't add new time table"));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace editor
|
||||
{
|
||||
osmoh::OpeningHours MakeOpeningHours(ui::TimeTableSet const & tts)
|
||||
{
|
||||
ASSERT_GREATER(tts.Size(), 0, ("At least one time table must present."));
|
||||
|
||||
if (tts.IsTwentyFourPerSeven())
|
||||
{
|
||||
osmoh::RuleSequence rulePart;
|
||||
rulePart.SetTwentyFourHours(true);
|
||||
return osmoh::OpeningHours({rulePart});
|
||||
}
|
||||
|
||||
osmoh::TRuleSequences rule;
|
||||
for (auto const & tt : tts)
|
||||
{
|
||||
osmoh::RuleSequence rulePart;
|
||||
rulePart.SetWeekdays(MakeWeekdays(tt));
|
||||
rulePart.SetTimes(MakeTimespans(tt));
|
||||
rule.push_back(rulePart);
|
||||
}
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
bool MakeTimeTableSet(osmoh::OpeningHours const & oh, ui::TimeTableSet & tts)
|
||||
{
|
||||
if (!oh.IsValid())
|
||||
return false;
|
||||
|
||||
if (oh.HasYearSelector() || oh.HasWeekSelector() || oh.HasMonthSelector())
|
||||
return false;
|
||||
|
||||
tts = ui::TimeTableSet();
|
||||
if (oh.IsTwentyFourHours())
|
||||
return true;
|
||||
|
||||
bool first = true;
|
||||
for (auto const & rulePart : oh.GetRule())
|
||||
{
|
||||
if (rulePart.IsEmpty())
|
||||
continue;
|
||||
|
||||
ui::TimeTable tt = ui::TimeTable::GetUninitializedTimeTable();
|
||||
tt.SetOpeningTime(tt.GetPredefinedOpeningTime());
|
||||
|
||||
// Comments and unknown rules belong to advanced mode.
|
||||
if (rulePart.GetModifier() == osmoh::RuleSequence::Modifier::Unknown ||
|
||||
rulePart.GetModifier() == osmoh::RuleSequence::Modifier::Comment)
|
||||
return false;
|
||||
|
||||
if (rulePart.GetModifier() == osmoh::RuleSequence::Modifier::Closed)
|
||||
{
|
||||
// Off modifier in the first part in oh is useless. Skip it.
|
||||
if (first == true)
|
||||
continue;
|
||||
|
||||
if (!ExcludeRulePart(rulePart, tts))
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rulePart.HasWeekdays())
|
||||
SetUpWeekdays(rulePart.GetWeekdays(), tt);
|
||||
else
|
||||
tt.SetOpeningDays(kWholeWeek);
|
||||
|
||||
auto const & times = rulePart.GetTimes();
|
||||
|
||||
bool isTwentyFourHours = times.empty() || (times.size() == 1 && times.front() == kTwentyFourHours);
|
||||
|
||||
if (isTwentyFourHours)
|
||||
{
|
||||
tt.SetTwentyFourHours(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
tt.SetTwentyFourHours(false);
|
||||
SetUpTimeTable(rulePart.GetTimes(), tt);
|
||||
}
|
||||
|
||||
// Check size as well since ExcludeRulePart can add new time tables.
|
||||
bool const appended = first && tts.Size() == 1 ? tts.Replace(tt, 0) : tts.Append(tt);
|
||||
first = false;
|
||||
if (!appended)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if no OH rule has been correctly processed.
|
||||
if (first)
|
||||
{
|
||||
// No OH rule has been correctly processed.
|
||||
// Set OH parsing as invalid.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace editor
|
||||
14
libs/editor/ui2oh.hpp
Normal file
14
libs/editor/ui2oh.hpp
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include "editor/opening_hours_ui.hpp"
|
||||
|
||||
namespace osmoh
|
||||
{
|
||||
class OpeningHours;
|
||||
} // namespace osmoh
|
||||
|
||||
namespace editor
|
||||
{
|
||||
osmoh::OpeningHours MakeOpeningHours(ui::TimeTableSet const & tts);
|
||||
bool MakeTimeTableSet(osmoh::OpeningHours const & oh, ui::TimeTableSet & tts);
|
||||
} // namespace editor
|
||||
1036
libs/editor/xml_feature.cpp
Normal file
1036
libs/editor/xml_feature.cpp
Normal file
File diff suppressed because it is too large
Load diff
226
libs/editor/xml_feature.hpp
Normal file
226
libs/editor/xml_feature.hpp
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
#pragma once
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
#include "geometry/point2d.hpp"
|
||||
#include "indexer/edit_journal.hpp"
|
||||
#include "indexer/feature_decl.hpp"
|
||||
|
||||
#include "coding/string_utf8_multilang.hpp"
|
||||
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#include <pugixml.hpp>
|
||||
|
||||
namespace osm
|
||||
{
|
||||
class EditableMapObject;
|
||||
}
|
||||
|
||||
namespace editor
|
||||
{
|
||||
DECLARE_EXCEPTION(XMLFeatureError, RootException);
|
||||
DECLARE_EXCEPTION(InvalidXML, XMLFeatureError);
|
||||
DECLARE_EXCEPTION(NoLatLon, XMLFeatureError);
|
||||
DECLARE_EXCEPTION(NoXY, XMLFeatureError);
|
||||
DECLARE_EXCEPTION(NoTimestamp, XMLFeatureError);
|
||||
DECLARE_EXCEPTION(NoHeader, XMLFeatureError);
|
||||
DECLARE_EXCEPTION(InvalidJournalEntry, XMLFeatureError);
|
||||
|
||||
class XMLFeature
|
||||
{
|
||||
static constexpr std::string_view kDefaultName = "name";
|
||||
static constexpr std::string_view kLocalName = "name:";
|
||||
static constexpr std::string_view kIntlName = "int_name";
|
||||
static constexpr std::string_view kAltName = "alt_name";
|
||||
static constexpr std::string_view kOldName = "old_name";
|
||||
static constexpr std::string_view kDefaultLang = "default";
|
||||
static constexpr std::string_view kIntlLang = kIntlName;
|
||||
static constexpr std::string_view kAltLang = kAltName;
|
||||
static constexpr std::string_view kOldLang = kOldName;
|
||||
|
||||
public:
|
||||
// Used in point to string serialization.
|
||||
static constexpr int kLatLonTolerance = 7;
|
||||
|
||||
enum class Type
|
||||
{
|
||||
Unknown,
|
||||
Node,
|
||||
Way,
|
||||
Relation
|
||||
};
|
||||
|
||||
/// Creates empty node or way.
|
||||
XMLFeature(Type const type);
|
||||
XMLFeature(std::string const & xml);
|
||||
XMLFeature(pugi::xml_document const & xml);
|
||||
XMLFeature(pugi::xml_node const & xml);
|
||||
XMLFeature(XMLFeature const & feature);
|
||||
|
||||
XMLFeature & operator=(XMLFeature const & feature);
|
||||
|
||||
// TODO: It should make "deep" compare instead of converting to strings.
|
||||
// Strings comparison does not work if tags order is different but tags are equal.
|
||||
bool operator==(XMLFeature const & other) const;
|
||||
/// @returns nodes, ways and relations from osmXml. Vector can be empty.
|
||||
static std::vector<XMLFeature> FromOSM(std::string const & osmXml);
|
||||
|
||||
void Save(std::ostream & ost) const;
|
||||
std::string ToOSMString() const;
|
||||
|
||||
/// Tags from featureWithChanges are applied to this(osm) feature.
|
||||
void ApplyPatch(XMLFeature const & featureWithChanges);
|
||||
|
||||
Type GetType() const;
|
||||
std::string GetTypeString() const;
|
||||
|
||||
m2::PointD GetMercatorCenter() const;
|
||||
ms::LatLon GetCenter() const;
|
||||
void SetCenter(ms::LatLon const & ll);
|
||||
void SetCenter(m2::PointD const & mercatorCenter);
|
||||
|
||||
std::vector<m2::PointD> GetGeometry() const;
|
||||
|
||||
/// Sets geometry in mercator to match against FeatureType's geometry in mwm
|
||||
/// when megrating to a new mwm build.
|
||||
/// Geometry points are now stored in <nd x="..." y="..." /> nodes like in osm <way>.
|
||||
/// But they are not the same as osm's. I.e. osm's one stores reference to a <node>
|
||||
/// with it's own data and lat, lon. Here we store only cooridanes in mercator.
|
||||
template <typename Iterator>
|
||||
void SetGeometry(Iterator begin, Iterator end)
|
||||
{
|
||||
ASSERT_NOT_EQUAL(GetType(), Type::Unknown, ());
|
||||
ASSERT_NOT_EQUAL(GetType(), Type::Node, ());
|
||||
|
||||
for (; begin != end; ++begin)
|
||||
{
|
||||
auto nd = GetRootNode().append_child("nd");
|
||||
nd.append_attribute("x") = strings::to_string_dac(begin->x, kLatLonTolerance).data();
|
||||
nd.append_attribute("y") = strings::to_string_dac(begin->y, kLatLonTolerance).data();
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Collection>
|
||||
void SetGeometry(Collection const & geometry)
|
||||
{
|
||||
SetGeometry(begin(geometry), end(geometry));
|
||||
}
|
||||
|
||||
std::string GetName(std::string_view lang) const;
|
||||
std::string GetName(uint8_t const langCode = StringUtf8Multilang::kDefaultCode) const;
|
||||
|
||||
template <typename Fn>
|
||||
void ForEachName(Fn && func) const
|
||||
{
|
||||
size_t const kPrefixLen = kLocalName.size();
|
||||
|
||||
for (auto const & tag : GetRootNode().select_nodes("tag"))
|
||||
{
|
||||
std::string_view const key = tag.node().attribute("k").value();
|
||||
|
||||
if (key.substr(0, kPrefixLen) == kLocalName)
|
||||
func(key.substr(kPrefixLen), tag.node().attribute("v").value());
|
||||
else if (key == kDefaultName)
|
||||
func(kDefaultLang, tag.node().attribute("v").value());
|
||||
else if (key == kIntlName)
|
||||
func(kIntlLang, tag.node().attribute("v").value());
|
||||
else if (key == kAltName)
|
||||
func(kAltLang, tag.node().attribute("v").value());
|
||||
else if (key == kOldName)
|
||||
func(kOldLang, tag.node().attribute("v").value());
|
||||
}
|
||||
}
|
||||
|
||||
void SetName(std::string_view name);
|
||||
void SetName(std::string_view lang, std::string_view name);
|
||||
void SetName(uint8_t const langCode, std::string_view name);
|
||||
|
||||
std::string GetHouse() const;
|
||||
void SetHouse(std::string const & house);
|
||||
|
||||
std::string GetCuisine() const;
|
||||
void SetCuisine(std::string cuisine);
|
||||
|
||||
/// Our and OSM modification time are equal.
|
||||
time_t GetModificationTime() const;
|
||||
void SetModificationTime(time_t const time);
|
||||
|
||||
/// @name XML storage format helpers.
|
||||
//@{
|
||||
uint32_t GetMWMFeatureIndex() const;
|
||||
void SetMWMFeatureIndex(uint32_t index);
|
||||
|
||||
/// @returns base::INVALID_TIME_STAMP if there were no any upload attempt.
|
||||
time_t GetUploadTime() const;
|
||||
void SetUploadTime(time_t const time);
|
||||
|
||||
std::string GetUploadStatus() const;
|
||||
void SetUploadStatus(std::string const & status);
|
||||
|
||||
std::string GetUploadError() const;
|
||||
void SetUploadError(std::string const & error);
|
||||
|
||||
osm::EditJournal GetEditJournal() const;
|
||||
void SetEditJournal(osm::EditJournal const & journal);
|
||||
//@}
|
||||
|
||||
bool HasAnyTags() const;
|
||||
bool HasTag(std::string_view key) const;
|
||||
bool HasAttribute(std::string_view key) const;
|
||||
bool HasKey(std::string_view key) const;
|
||||
|
||||
template <typename Fn>
|
||||
void ForEachTag(Fn && func) const
|
||||
{
|
||||
for (auto const & tag : GetRootNode().select_nodes("tag"))
|
||||
func(tag.node().attribute("k").value(), tag.node().attribute("v").value());
|
||||
}
|
||||
|
||||
std::string GetTagValue(std::string_view key) const;
|
||||
void SetTagValue(std::string_view key, std::string_view value);
|
||||
void RemoveTag(std::string_view key);
|
||||
|
||||
/// Wrapper for SetTagValue and RemoveTag, avoids duplication for similar alternative osm tags
|
||||
void UpdateOSMTag(std::string_view key, std::string_view value);
|
||||
/// Replace an old business with a new business
|
||||
void OSMBusinessReplacement(uint32_t old_type, uint32_t new_type);
|
||||
|
||||
std::string GetAttribute(std::string const & key) const;
|
||||
void SetAttribute(std::string const & key, std::string const & value);
|
||||
|
||||
bool AttachToParentNode(pugi::xml_node parent) const;
|
||||
|
||||
static std::string TypeToString(Type type);
|
||||
static Type StringToType(std::string const & type);
|
||||
|
||||
private:
|
||||
pugi::xml_node const GetRootNode() const;
|
||||
pugi::xml_node GetRootNode();
|
||||
|
||||
pugi::xml_document m_document;
|
||||
};
|
||||
|
||||
/// Rewrites all but geometry and types.
|
||||
/// Should be applied to existing features only (in mwm files).
|
||||
void ApplyPatch(XMLFeature const & xml, osm::EditableMapObject & object);
|
||||
|
||||
/// @param serializeType if false, types are not serialized.
|
||||
/// Useful for applying modifications to existing OSM features, to avoid issues when someone
|
||||
/// has changed a type in OSM, but our users uploaded invalid outdated type after modifying feature.
|
||||
XMLFeature ToXML(osm::EditableMapObject const & object, bool serializeType);
|
||||
|
||||
/// Used to generate XML for created objects in the new editor
|
||||
XMLFeature TypeToXML(uint32_t type, feature::GeomType geomType, m2::PointD mercator);
|
||||
|
||||
/// Creates new feature, including geometry and types.
|
||||
/// @Note: only nodes (points) are supported at the moment.
|
||||
bool FromXML(XMLFeature const & xml, osm::EditableMapObject & object);
|
||||
|
||||
std::string DebugPrint(XMLFeature const & feature);
|
||||
std::string DebugPrint(XMLFeature::Type const type);
|
||||
} // namespace editor
|
||||
26
libs/editor/yes_no_unknown.hpp
Normal file
26
libs/editor/yes_no_unknown.hpp
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
/// Used to store and edit 3-state OSM information, for example,
|
||||
/// "This place has internet", "does not have", or "it's not specified yet".
|
||||
/// Explicit values are given for easier reuse in Java code.
|
||||
namespace osm
|
||||
{
|
||||
enum YesNoUnknown
|
||||
{
|
||||
Unknown = 0,
|
||||
Yes = 1,
|
||||
No = 2
|
||||
};
|
||||
|
||||
inline std::string DebugPrint(YesNoUnknown value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case Unknown: return "Unknown";
|
||||
case Yes: return "Yes";
|
||||
case No: return "No";
|
||||
}
|
||||
}
|
||||
} // namespace osm
|
||||
Loading…
Add table
Add a link
Reference in a new issue