Repo created

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

View 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)

View 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

View 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

View 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

View 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

View 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>()) {}
};

View 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);
}

View 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);
}
};

View 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

View 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

View 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

View 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

View 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

View 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

View 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
)

View 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

View 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), ());
}

View 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, ());
}
}

View 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

View 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

View 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"));
}
}
}

View 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(), ());
}
}

File diff suppressed because it is too large Load diff

View 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

View 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",
());
}
}

View 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", ());
}
}

View 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)

View 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

View 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

View 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

View 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

View 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

View 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

View 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",
};

View file

@ -0,0 +1,6 @@
module OSMEditor {
header "osm_auth.hpp"
header "osm_editor.hpp"
header "server_api.hpp"
requires cplusplus
}

View 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

View 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

View 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

View 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
View 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
View 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

View 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)

View 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

View 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

File diff suppressed because it is too large Load diff

271
libs/editor/osm_editor.hpp Normal file
View 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
View 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

View 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
View 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
View 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

File diff suppressed because it is too large Load diff

226
libs/editor/xml_feature.hpp Normal file
View 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

View 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