OSM Server API in our client to upload edits.

This commit is contained in:
Alex Zolotarev 2015-12-15 12:36:21 +03:00 committed by Sergey Yershov
parent 12f01e0ab9
commit 5da5d0b348
5 changed files with 362 additions and 1 deletions

View file

@ -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 \

View file

@ -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 \

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