forked from organicmaps/organicmaps
OSM Server API in our client to upload edits.
This commit is contained in:
parent
12f01e0ab9
commit
5da5d0b348
5 changed files with 362 additions and 1 deletions
|
@ -10,10 +10,12 @@ include($$ROOT_DIR/common.pri)
|
|||
|
||||
SOURCES += \
|
||||
opening_hours_ui.cpp \
|
||||
server_api.cpp \
|
||||
ui2oh.cpp \
|
||||
xml_feature.cpp \
|
||||
|
||||
HEADERS += \
|
||||
opening_hours_ui.hpp \
|
||||
server_api.hpp \
|
||||
ui2oh.hpp \
|
||||
xml_feature.hpp \
|
||||
|
|
|
@ -4,7 +4,7 @@ CONFIG -= app_bundle
|
|||
TEMPLATE = app
|
||||
|
||||
ROOT_DIR = ../..
|
||||
DEPENDENCIES = editor geometry coding base opening_hours pugixml
|
||||
DEPENDENCIES = editor geometry coding base stats_client opening_hours pugixml
|
||||
|
||||
include($$ROOT_DIR/common.pri)
|
||||
|
||||
|
@ -15,5 +15,6 @@ HEADERS += \
|
|||
SOURCES += \
|
||||
$$ROOT_DIR/testing/testingmain.cpp \
|
||||
opening_hours_ui_test.cpp \
|
||||
server_api_test.cpp \
|
||||
xml_feature_test.cpp \
|
||||
ui2oh_test.cpp \
|
||||
|
|
123
editor/editor_tests/server_api_test.cpp
Normal file
123
editor/editor_tests/server_api_test.cpp
Normal file
|
@ -0,0 +1,123 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "editor/server_api.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "3party/pugixml/src/pugixml.hpp"
|
||||
|
||||
using osm::ServerApi06;
|
||||
using namespace pugi;
|
||||
|
||||
constexpr char const * kOsmDevServer = "http://master.apis.dev.openstreetmap.org";
|
||||
constexpr char const * kValidOsmUser = "MapsMeTestUser";
|
||||
constexpr char const * kInvalidOsmUser = "qwesdxzcgretwr";
|
||||
ServerApi06 const kApi(kValidOsmUser, "12345678", kOsmDevServer);
|
||||
|
||||
UNIT_TEST(OSM_ServerAPI_CheckUserAndPassword)
|
||||
{
|
||||
TEST(kApi.CheckUserAndPassword(), ());
|
||||
|
||||
my::LogLevelSuppressor s;
|
||||
TEST(!ServerApi06(kInvalidOsmUser, "3345dfce2", kOsmDevServer).CheckUserAndPassword(), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(OSM_ServerAPI_HttpCodeForUrl)
|
||||
{
|
||||
TEST_EQUAL(200, ServerApi06::HttpCodeForUrl(string(kOsmDevServer) + "/user/" + kValidOsmUser), ());
|
||||
TEST_EQUAL(404, ServerApi06::HttpCodeForUrl(string(kOsmDevServer) + "/user/" + kInvalidOsmUser), ());
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// id attribute is set to -1.
|
||||
// version attribute is set to 1.
|
||||
void GenerateNodeXml(double lat, double lon, ServerApi06::TKeyValueTags const & tags, xml_document & outNode)
|
||||
{
|
||||
outNode.reset();
|
||||
xml_node node = outNode.append_child("osm").append_child("node");
|
||||
node.append_attribute("id") = -1;
|
||||
node.append_attribute("version") = 1;
|
||||
node.append_attribute("lat") = lat;
|
||||
node.append_attribute("lon") = lon;
|
||||
for (auto const & kv : tags)
|
||||
{
|
||||
xml_node tag = node.append_child("tag");
|
||||
tag.append_attribute("k").set_value(kv.first.c_str());
|
||||
tag.append_attribute("v").set_value(kv.second.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
string XmlToString(xml_document const & doc)
|
||||
{
|
||||
ostringstream stream;
|
||||
doc.print(stream);
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
// Replaces attribute for <node> tag (creates attribute if it's not present).
|
||||
template <class TNewValue>
|
||||
bool SetAttributeForOsmNode(xml_document & doc, char const * attribute, TNewValue const & v)
|
||||
{
|
||||
xml_node node = doc.find_node([](xml_node const & n)
|
||||
{
|
||||
return 0 == strncmp(n.name(), "node", sizeof("node"));
|
||||
});
|
||||
if (node.empty())
|
||||
return false;
|
||||
if (node.attribute(attribute).empty())
|
||||
node.append_attribute(attribute) = v;
|
||||
else
|
||||
node.attribute(attribute) = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UNIT_TEST(SetAttributeForOsmNode)
|
||||
{
|
||||
xml_document doc;
|
||||
doc.append_child("osm").append_child("node");
|
||||
|
||||
TEST(SetAttributeForOsmNode(doc, "Test", 123), ());
|
||||
TEST_EQUAL(123, doc.child("osm").child("node").attribute("Test").as_int(), ());
|
||||
|
||||
TEST(SetAttributeForOsmNode(doc, "Test", 321), ());
|
||||
TEST_EQUAL(321, doc.child("osm").child("node").attribute("Test").as_int(), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(OSM_ServerAPI_ChangesetActions)
|
||||
{
|
||||
uint64_t changeSetId;
|
||||
TEST(kApi.CreateChangeSet({{"created_by", "MAPS.ME Unit Test"}, {"comment", "For test purposes only."}}, changeSetId), ());
|
||||
|
||||
xml_document node;
|
||||
GenerateNodeXml(11.11, 12.12, {{"testkey", "firstnode"}}, node);
|
||||
TEST(SetAttributeForOsmNode(node, "changeset", changeSetId), ());
|
||||
|
||||
uint64_t nodeId;
|
||||
TEST(kApi.CreateNode(XmlToString(node), nodeId), ());
|
||||
TEST(SetAttributeForOsmNode(node, "id", nodeId), ());
|
||||
|
||||
TEST(SetAttributeForOsmNode(node, "lat", 10.10), ());
|
||||
TEST(kApi.ModifyNode(XmlToString(node), nodeId), ());
|
||||
// After modification, node version has increased.
|
||||
TEST(SetAttributeForOsmNode(node, "version", 2), ());
|
||||
|
||||
// To retrieve created node, changeset should be closed first.
|
||||
TEST(kApi.CloseChangeSet(changeSetId), ());
|
||||
|
||||
TEST(kApi.CreateChangeSet({{"created_by", "MAPS.ME Unit Test"}, {"comment", "For test purposes only."}}, changeSetId), ());
|
||||
// New changeset has new id.
|
||||
TEST(SetAttributeForOsmNode(node, "changeset", changeSetId), ());
|
||||
|
||||
string const serverReply = kApi.GetXmlNodeByLatLon(node.child("osm").child("node").attribute("lat").as_double(),
|
||||
node.child("osm").child("node").attribute("lon").as_double());
|
||||
xml_document reply;
|
||||
reply.load_string(serverReply.c_str());
|
||||
TEST_EQUAL(nodeId, reply.child("osm").child("node").attribute("id").as_ullong(), ());
|
||||
|
||||
TEST(ServerApi06::DeleteResult::ESuccessfullyDeleted == kApi.DeleteNode(XmlToString(node), nodeId), ());
|
||||
|
||||
TEST(kApi.CloseChangeSet(changeSetId), ());
|
||||
}
|
180
editor/server_api.cpp
Normal file
180
editor/server_api.cpp
Normal file
|
@ -0,0 +1,180 @@
|
|||
#include "editor/server_api.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include "std/sstream.hpp"
|
||||
|
||||
#include "3party/Alohalytics/src/http_client.h"
|
||||
|
||||
using alohalytics::HTTPClientPlatformWrapper;
|
||||
|
||||
namespace osm
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
void PrintRequest(HTTPClientPlatformWrapper const & r)
|
||||
{
|
||||
LOG(LINFO, ("HTTP", r.http_method(), r.url_requested(), "has finished with code", r.error_code(),
|
||||
(r.was_redirected() ? ", was redirected to " + r.url_received() : ""),
|
||||
"Server replied:\n", r.server_response()));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ServerApi06::ServerApi06(string const & user, string const & password, string const & baseUrl)
|
||||
: m_user(user), m_password(password), m_baseOsmServerUrl(baseUrl)
|
||||
{
|
||||
}
|
||||
|
||||
bool ServerApi06::CreateChangeSet(TKeyValueTags const & kvTags, uint64_t & outChangeSetId) const
|
||||
{
|
||||
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";
|
||||
|
||||
HTTPClientPlatformWrapper request(m_baseOsmServerUrl + "/api/0.6/changeset/create");
|
||||
bool const success = request.set_user_and_password(m_user, m_password)
|
||||
.set_body_data(move(stream.str()), "", "PUT")
|
||||
.RunHTTPRequest();
|
||||
if (success && request.error_code() == 200)
|
||||
{
|
||||
if (strings::to_uint64(request.server_response(), outChangeSetId))
|
||||
return true;
|
||||
LOG(LWARNING, ("Can't parse changeset ID from server response."));
|
||||
}
|
||||
else
|
||||
LOG(LWARNING, ("CreateChangeSet request has failed."));
|
||||
|
||||
PrintRequest(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ServerApi06::CreateNode(string const & nodeXml, uint64_t & outCreatedNodeId) const
|
||||
{
|
||||
HTTPClientPlatformWrapper request(m_baseOsmServerUrl + "/api/0.6/node/create");
|
||||
bool const success = request.set_user_and_password(m_user, m_password)
|
||||
.set_body_data(move(nodeXml), "", "PUT")
|
||||
.RunHTTPRequest();
|
||||
if (success && request.error_code() == 200)
|
||||
{
|
||||
if (strings::to_uint64(request.server_response(), outCreatedNodeId))
|
||||
return true;
|
||||
LOG(LWARNING, ("Can't parse created node ID from server response."));
|
||||
}
|
||||
else
|
||||
LOG(LWARNING, ("CreateNode request has failed."));
|
||||
|
||||
PrintRequest(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ServerApi06::ModifyNode(string const & nodeXml, uint64_t nodeId) const
|
||||
{
|
||||
HTTPClientPlatformWrapper request(m_baseOsmServerUrl + "/api/0.6/node/" + strings::to_string(nodeId));
|
||||
bool const success = request.set_user_and_password(m_user, m_password)
|
||||
.set_body_data(move(nodeXml), "", "PUT")
|
||||
.RunHTTPRequest();
|
||||
if (success && request.error_code() == 200)
|
||||
return true;
|
||||
|
||||
LOG(LWARNING, ("ModifyNode request has failed."));
|
||||
PrintRequest(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
ServerApi06::DeleteResult ServerApi06::DeleteNode(string const & nodeXml, uint64_t nodeId) const
|
||||
{
|
||||
HTTPClientPlatformWrapper request(m_baseOsmServerUrl + "/api/0.6/node/" + strings::to_string(nodeId));
|
||||
bool const success = request.set_user_and_password(m_user, m_password)
|
||||
.set_body_data(move(nodeXml), "", "DELETE")
|
||||
.RunHTTPRequest();
|
||||
if (success)
|
||||
{
|
||||
switch (request.error_code())
|
||||
{
|
||||
case 200: return DeleteResult::ESuccessfullyDeleted;
|
||||
case 412: return DeleteResult::ECanNotBeDeleted;
|
||||
}
|
||||
}
|
||||
|
||||
LOG(LWARNING, ("DeleteNode request has failed."));
|
||||
PrintRequest(request);
|
||||
return DeleteResult::EFailed;
|
||||
}
|
||||
|
||||
bool ServerApi06::CloseChangeSet(uint64_t changesetId) const
|
||||
{
|
||||
HTTPClientPlatformWrapper request(m_baseOsmServerUrl + "/api/0.6/changeset/" +
|
||||
strings::to_string(changesetId) + "/close");
|
||||
bool const success = request.set_user_and_password(m_user, m_password)
|
||||
.set_http_method("PUT")
|
||||
.RunHTTPRequest();
|
||||
if (success && request.error_code() == 200)
|
||||
return true;
|
||||
|
||||
LOG(LWARNING, ("CloseChangeSet request has failed."));
|
||||
PrintRequest(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ServerApi06::CheckUserAndPassword() const
|
||||
{
|
||||
static constexpr char const * kAPIWritePermission = "allow_write_api";
|
||||
HTTPClientPlatformWrapper request(m_baseOsmServerUrl + "/api/0.6/permissions");
|
||||
bool const success = request.set_user_and_password(m_user, m_password).RunHTTPRequest();
|
||||
if (success && request.error_code() == 200 &&
|
||||
request.server_response().find(kAPIWritePermission) != string::npos)
|
||||
return true;
|
||||
|
||||
LOG(LWARNING, ("OSM user and/or password are invalid."));
|
||||
PrintRequest(request);
|
||||
return false;
|
||||
}
|
||||
|
||||
int ServerApi06::HttpCodeForUrl(string const & url)
|
||||
{
|
||||
HTTPClientPlatformWrapper request(url);
|
||||
bool const success = request.RunHTTPRequest();
|
||||
int const httpCode = request.error_code();
|
||||
if (success)
|
||||
return httpCode;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
string ServerApi06::GetXmlFeaturesInRect(m2::RectD const & latLonRect) const
|
||||
{
|
||||
using strings::to_string_dac;
|
||||
|
||||
// Digits After Comma.
|
||||
static constexpr double const kDAC = 7;
|
||||
m2::PointD const lb = latLonRect.LeftBottom();
|
||||
m2::PointD const rt = latLonRect.RightTop();
|
||||
string const url = m_baseOsmServerUrl + "/api/0.6/map?bbox=" + to_string_dac(lb.x, kDAC) + ',' + to_string_dac(lb.y, kDAC) + ',' +
|
||||
to_string_dac(rt.x, kDAC) + ',' + to_string_dac(rt.y, kDAC);
|
||||
HTTPClientPlatformWrapper request(url);
|
||||
bool const success = request.set_user_and_password(m_user, m_password).RunHTTPRequest();
|
||||
if (success && request.error_code() == 200)
|
||||
return request.server_response();
|
||||
|
||||
LOG(LWARNING, ("GetXmlFeaturesInRect request has failed."));
|
||||
PrintRequest(request);
|
||||
return string();
|
||||
}
|
||||
|
||||
string ServerApi06::GetXmlNodeByLatLon(double lat, double lon) const
|
||||
{
|
||||
constexpr double const kInflateRadiusDegrees = 1.0e-6; //!< ~1 meter.
|
||||
m2::RectD rect(lon, lat, lon, lat);
|
||||
rect.Inflate(kInflateRadiusDegrees, kInflateRadiusDegrees);
|
||||
return GetXmlFeaturesInRect(rect);
|
||||
}
|
||||
|
||||
} // namespace osm
|
55
editor/server_api.hpp
Normal file
55
editor/server_api.hpp
Normal file
|
@ -0,0 +1,55 @@
|
|||
#pragma once
|
||||
|
||||
#include "geometry/rect2d.hpp"
|
||||
|
||||
#include "std/map.hpp"
|
||||
#include "std/string.hpp"
|
||||
|
||||
namespace osm
|
||||
{
|
||||
|
||||
/// All methods here are synchronous and need wrappers for async usage.
|
||||
/// TODO(AlexZ): Rewrite ServerAPI interface to accept XMLFeature.
|
||||
class ServerApi06
|
||||
{
|
||||
public:
|
||||
// k= and v= tags used in OSM.
|
||||
using TKeyValueTags = map<string, string>;
|
||||
|
||||
/// Some nodes can't be deleted if they are used in ways or relations.
|
||||
enum class DeleteResult
|
||||
{
|
||||
ESuccessfullyDeleted,
|
||||
EFailed,
|
||||
ECanNotBeDeleted
|
||||
};
|
||||
|
||||
ServerApi06(string const & user, string const & password, string const & baseUrl = "http://api.openstreetmap.org");
|
||||
/// @returns true if connection with OSM server was established, and user+password are valid.
|
||||
bool CheckUserAndPassword() const;
|
||||
/// @returns http server code for given url or negative value in case of error.
|
||||
/// This function can be used to check if user did not confirm email validation link after registration.
|
||||
/// For example, for http://www.openstreetmap.org/user/UserName 200 is returned if UserName was registered.
|
||||
static int HttpCodeForUrl(string const & url);
|
||||
|
||||
/// Please use at least created_by=* and comment=* tags.
|
||||
bool CreateChangeSet(TKeyValueTags const & kvTags, uint64_t & outChangeSetId) const;
|
||||
/// nodeXml should be wrapped into <osm> ... </osm> tags.
|
||||
bool CreateNode(string const & nodeXml, uint64_t & outCreatedNodeId) const;
|
||||
/// nodeXml should be wrapped into <osm> ... </osm> tags.
|
||||
bool ModifyNode(string const & nodeXml, uint64_t nodeId) const;
|
||||
/// nodeXml should be wrapped into <osm> ... </osm> tags.
|
||||
DeleteResult DeleteNode(string const & nodeXml, uint64_t nodeId) const;
|
||||
bool CloseChangeSet(uint64_t changesetId) const;
|
||||
|
||||
/// @returns OSM xml string with features in the bounding box or empty string on error.
|
||||
string GetXmlFeaturesInRect(m2::RectD const & latLonRect) const;
|
||||
string GetXmlNodeByLatLon(double lat, double lon) const;
|
||||
|
||||
private:
|
||||
string m_user;
|
||||
string m_password;
|
||||
string m_baseOsmServerUrl;
|
||||
};
|
||||
|
||||
} // namespace osm
|
Loading…
Add table
Reference in a new issue