diff --git a/map/ge0_parser.cpp b/map/ge0_parser.cpp new file mode 100644 index 0000000000..66e953fa80 --- /dev/null +++ b/map/ge0_parser.cpp @@ -0,0 +1,140 @@ +#include "ge0_parser.hpp" +#include "url_api.hpp" +#include "../api/internal/c/api-client-internals.h" +#include "../coding/url_encode.hpp" +#include "../base/math.hpp" + +static const int NAME_POSITON_IN_URL = 17; +static const int ZOOM_POSITION = 6; + +url_api::Ge0Parser::Ge0Parser() +{ + for (size_t i = 0; i < 256; ++i) + m_base64ReverseCharTable[i] = 255; + for (uint8_t i = 0; i < 64; ++i) + { + char c = MapsWithMe_Base64Char(i); + m_base64ReverseCharTable[static_cast(c)] = i; + } +} + +bool url_api::Ge0Parser::Parse(string const & url, Request & request) +{ + // URL format: + // + // +------------------ 1 byte: zoom level + // |+-------+--------- 9 bytes: lat,lon + // || | +--+---- Variable number of bytes: point name + // || | | | + // ge0://ZCoordba64/Name + + request.Clear(); + if (url.size() < 16 || url.substr(0, 6) != "ge0://") + return false; + + uint8_t const zoomI = DecodeBase64Char(url[ZOOM_POSITION]); + if (zoomI > 63) + return false; + request.m_viewportZoomLevel = DecodeZoom(zoomI); + + request.m_points.push_back(url_api::Point()); + url_api::Point & newPt = request.m_points.back(); + + DecodeLatLon(url.substr(7,9), newPt.m_lat, newPt.m_lon); + + request.m_viewportLat = newPt.m_lat; + request.m_viewportLon = newPt.m_lon; + + if (url.size() >= NAME_POSITON_IN_URL) + newPt.m_name = DecodeName(url.substr(NAME_POSITON_IN_URL, url.size() - NAME_POSITON_IN_URL)); + return true; +} + +uint8_t url_api::Ge0Parser::DecodeBase64Char(char const c) +{ + return m_base64ReverseCharTable[static_cast(c)]; +} + +double url_api::Ge0Parser::DecodeZoom(uint8_t const zoomByte) +{ + //Coding zoom - int newZoom = ((oldZoom - 4) * 4) + return static_cast(zoomByte) / 4 + 4; +} + +void url_api::Ge0Parser::DecodeLatLon(string const & url, double & lat, double & lon) +{ + int latInt = 0, lonInt = 0; + DecodeLatLonToInt(url, latInt, lonInt, url.size()); + lat = DecodeLatFromInt(latInt, (1 << MAPSWITHME_MAX_COORD_BITS) - 1); + lon = DecodeLonFromInt(lonInt, (1 << MAPSWITHME_MAX_COORD_BITS) - 1); +} + +void url_api::Ge0Parser::DecodeLatLonToInt(string const & url, int & lat, int & lon, int const bytes) +{ + for(int i = 0, shift = MAPSWITHME_MAX_COORD_BITS - 3; i < bytes; ++i, shift -= 3) + { + const uint8_t a = DecodeBase64Char(url[i]); + const int lat1 = (((a >> 5) & 1) << 2 | + ((a >> 3) & 1) << 1 | + ((a >> 1) & 1)); + const int lon1 = (((a >> 4) & 1) << 2 | + ((a >> 2) & 1) << 1 | + (a & 1)); + lat |= lat1 << shift; + lon |= lon1 << shift; + } + const double middleOfSquare = 1 << (3 * (MAPSWITHME_MAX_POINT_BYTES - bytes) - 1); + lat += middleOfSquare; + lon += middleOfSquare; +} + +double url_api::Ge0Parser::DecodeLatFromInt(int const lat, int const maxValue) +{ + return static_cast(lat) / maxValue * 180 - 90; +} + +double url_api::Ge0Parser::DecodeLonFromInt(int const lon, int const maxValue) +{ + return static_cast(lon) / (maxValue + 1.0) * 360.0 - 180; +} + +string url_api::Ge0Parser::DecodeName(string const & name) +{ + string resultName = name; + ValidateName(resultName); + resultName = UrlDecode(resultName); + SpacesToUnderscore(resultName); + return resultName; +} + +void url_api::Ge0Parser::SpacesToUnderscore(string & name) +{ + for (size_t i = 0; i < name.size(); ++i) + if (name[i] == ' ') + name[i] = '_'; + else if (name[i] == '_') + name[i] = ' '; +} + +void url_api::Ge0Parser::ValidateName(string & name) +{ + if (name.empty()) + return; + for (size_t i = 0; i + 2 < name.size(); ++i) + { + if (name[i] == '%' && (!IsHexChar(name[i + 1]) || !IsHexChar(name[i + 2]))) + { + name.resize(i); + return; + } + } + if (name[name.size() - 1] == '%') + name.resize(name.size() - 1); + else if (name.size() > 1 && name[name.size() - 2] == '%') + name.resize(name.size() - 2); +} + +bool url_api::Ge0Parser::IsHexChar(char const a) +{ + return ((a >= '0' && a <= '9') || (a >= 'A' && a <= 'F') || (a >= 'a' && a <= 'f')); +} diff --git a/map/ge0_parser.hpp b/map/ge0_parser.hpp new file mode 100644 index 0000000000..fabfa1f6b0 --- /dev/null +++ b/map/ge0_parser.hpp @@ -0,0 +1,33 @@ +#pragma once +#include "../base/base.hpp" +#include "../std/string.hpp" + +namespace url_api +{ + +class Request; + +class Ge0Parser +{ +public: + Ge0Parser(); + + bool Parse(string const & url, Request & request); + +protected: + uint8_t DecodeBase64Char(char const c); + static double DecodeZoom(uint8_t const zoomByte); + void DecodeLatLon(string const & url, double & lat, double & lon); + void DecodeLatLonToInt(string const & url, int & lat, int & lon, int const bytes); + double DecodeLatFromInt(int const lat, int const maxValue); + double DecodeLonFromInt(int const lon, int const maxValue); + string DecodeName(string const & name); + void SpacesToUnderscore(string & name); + void ValidateName(string & name); + static bool IsHexChar(char const a); + +private: + uint8_t m_base64ReverseCharTable[256]; +}; + +} // namespace url_api diff --git a/map/map.pro b/map/map.pro index 0e7c993961..46ce6a9011 100644 --- a/map/map.pro +++ b/map/map.pro @@ -56,6 +56,8 @@ HEADERS += \ area_info.hpp \ geometry_processors.hpp \ bookmark_manager.hpp \ + ge0_parser.hpp \ + url_api.hpp \ SOURCES += \ feature_vec_model.cpp \ @@ -102,6 +104,8 @@ SOURCES += \ feature_info.cpp \ geometry_processors.cpp \ bookmark_manager.cpp \ + ge0_parser.cpp \ + ../api/src/c/api-client.c \ !iphone*:!bada*:!android* { HEADERS += qgl_render_context.hpp diff --git a/map/map_tests/ge0_parser_tests.cpp b/map/map_tests/ge0_parser_tests.cpp new file mode 100644 index 0000000000..ebee3eb5fa --- /dev/null +++ b/map/map_tests/ge0_parser_tests.cpp @@ -0,0 +1,248 @@ +#include "../../testing/testing.hpp" + +#include "../ge0_parser.hpp" +#include "../url_api.hpp" + +#include "../../api/internal/c/api-client-internals.h" +#include "../../api/src/c/api-client.h" + +#include "../../base/macros.hpp" + + +using url_api::Ge0Parser; +using url_api::Request; + +namespace +{ + +class Ge0ParserForTest : public Ge0Parser +{ +public: + using Ge0Parser::DecodeBase64Char; + using Ge0Parser::DecodeZoom; + using Ge0Parser::DecodeLatLon; +}; + +double GetLatEpsilon(int coordBytes) +{ + // Should be / 2.0 but probably because of accumulates loss of precision, 1.77 works but 2.0 doesn't. + double infelicity = 1 << ((MAPSWITHME_MAX_POINT_BYTES - coordBytes) * 3); + return infelicity / ((1 << MAPSWITHME_MAX_COORD_BITS) - 1) * 180 / 1.77; +} + +double GetLonEpsilon(int coordBytes) +{ + // Should be / 2.0 but probably because of accumulates loss of precision, 1.77 works but 2.0 doesn't. + double infelicity = 1 << ((MAPSWITHME_MAX_POINT_BYTES - coordBytes) * 3); + return (infelicity / ((1 << MAPSWITHME_MAX_COORD_BITS) - 1)) * 360 / 1.77; +} + +void TestSuccess(char const * s, double lat, double lon, double zoom, char const * name) +{ + Ge0Parser parser; + Request request; + bool const result = parser.Parse(s, request); + + TEST(result, (s, zoom, lat, lon, name)); + + TEST_EQUAL(request.m_points.size(), 1, (s, zoom, lat, lon, name)); + TEST_EQUAL(request.m_points[0].m_name, string(name), (s)); + TEST_EQUAL(request.m_points[0].m_id, string(), (s)); + double const latEps = GetLatEpsilon(9); + double const lonEps = GetLonEpsilon(9); + TEST(fabs(request.m_points[0].m_lat - lat) <= latEps, (s, zoom, lat, lon, name)); + TEST(fabs(request.m_points[0].m_lon - lon) <= lonEps, (s, zoom, lat, lon, name)); + + TEST(fabs(request.m_viewportLat - lat) <= latEps, (s, zoom, lat, lon, name)); + TEST(fabs(request.m_viewportLon - lon) <= lonEps, (s, zoom, lat, lon, name)); + TEST_ALMOST_EQUAL(request.m_viewportZoomLevel, zoom, (s, zoom, lat, lon, name)); +} + +void TestFailure(char const * s) +{ + Ge0Parser parser; + Request request; + bool const result = parser.Parse(s, request); + TEST_EQUAL(result, false, (s)); +} + +bool ConvergenceTest(double lat, double lon, double latEps, double lonEps) +{ + double tmpLat = lat, tmpLon = lon; + Ge0ParserForTest parser; + for (size_t i = 0; i < 100000; ++i) + { + char urlPrefix[] = "Coord6789"; + MapsWithMe_LatLonToString(tmpLat, tmpLon, urlPrefix + 0, 9); + parser.DecodeLatLon(urlPrefix, tmpLat, tmpLon); + } + if (abs(lat - tmpLat) <= latEps && abs(lon - tmpLon) <= lonEps) + return true; + return false; +} + +} // unnamed namespace + +UNIT_TEST(Base64DecodingWorksForAValidChar) +{ + Ge0ParserForTest parser; + for (size_t i = 0; i < 64; ++i) + { + char c = MapsWithMe_Base64Char(i); + int i1 = parser.DecodeBase64Char(c); + TEST_EQUAL(i, i1, (c)); + } +} + +UNIT_TEST(Base64DecodingReturns255ForInvalidChar) +{ + Ge0ParserForTest parser; + TEST_EQUAL(parser.DecodeBase64Char(' '), 255, ()); +} + +UNIT_TEST(Base64DecodingDoesNotCrashForAllChars) +{ + Ge0ParserForTest parser; + for (size_t i = 0; i < 256; ++i) + parser.DecodeBase64Char(static_cast(i)); +} + +UNIT_TEST(Base64DecodingCharFrequency) +{ + vector charCounts(256, 0); + Ge0ParserForTest parser; + for (size_t i = 0; i < 256; ++i) + ++charCounts[parser.DecodeBase64Char(static_cast(i))]; + sort(charCounts.begin(), charCounts.end()); + TEST_EQUAL(charCounts[255], 256 - 64, ()); + TEST_EQUAL(charCounts[254], 1, ()); + TEST_EQUAL(charCounts[254 - 63], 1, ()); + TEST_EQUAL(charCounts[254 - 64], 0, ()); + TEST_EQUAL(charCounts[0], 0, ()); +} + +UNIT_TEST(UrlSchemaValidationFailed) +{ + TestFailure("trali vali"); + TestFailure("trali vali tili tili eto my prohodili"); +} + +UNIT_TEST(DecodeZoomLevel) +{ + TEST_EQUAL(Ge0ParserForTest::DecodeZoom(0), 4, ()); + TEST_EQUAL(Ge0ParserForTest::DecodeZoom(4), 5, ()); + TEST_EQUAL(Ge0ParserForTest::DecodeZoom(6), 5.5, ()); + TEST_EQUAL(Ge0ParserForTest::DecodeZoom(53), 17.25, ()); + TEST_EQUAL(Ge0ParserForTest::DecodeZoom(60), 19, ()); + TEST_EQUAL(Ge0ParserForTest::DecodeZoom(63), 19.75, ()); + TestFailure("ge0://!wAAAAAAAA/Name"); + TestFailure("ge0:///wAAAAAAAA/Name"); +} + +UNIT_TEST(LatLonConvergence) +{ + double const latEps = GetLatEpsilon(9); + double const lonEps = GetLonEpsilon(9); + TEST(ConvergenceTest(0, 0, latEps, lonEps), ()); + TEST(ConvergenceTest(1.111111, 2.11111, latEps, lonEps), ()); + TEST(ConvergenceTest(-1.111111, -2.11111, latEps, lonEps), ()); + TEST(ConvergenceTest(-90, -179.999999, latEps, lonEps), ()); + TEST(ConvergenceTest(-88.12313, 80.4532999999, latEps, lonEps), ()); +} + +UNIT_TEST(ZoomDecoding) +{ + TestSuccess("ge0://8wAAAAAAAA/Name", 0, 0, 19, "Name"); + TestSuccess("ge0://AwAAAAAAAA/Name", 0, 0, 4, "Name"); + TestSuccess("ge0://BwAAAAAAAA/Name", 0, 0, 4.25, "Name"); +} + +UNIT_TEST(LatLonDecoding) +{ + TestSuccess("ge0://Byqqqqqqqq/Name", 45, 0, 4.25, "Name"); + TestSuccess("ge0://B6qqqqqqqq/Name", 90, 0, 4.25, "Name"); + TestSuccess("ge0://BVVVVVVVVV/Name", -90, 179.999999, 4.25, "Name"); + TestSuccess("ge0://BP________/Name", -0.000001, -0.000001, 4.25, "Name"); + TestSuccess("ge0://B_________/Name", 90, 179.999999, 4.25, "Name"); + TestSuccess("ge0://Bqqqqqqqqq/Name", 90, -180, 4.25, "Name"); + TestSuccess("ge0://BAAAAAAAAA/Name", -90, -180, 4.25, "Name"); +} + +UNIT_TEST(NameDecoding) +{ + TestSuccess("ge0://AwAAAAAAAA/Super_Poi", 0, 0, 4, "Super Poi"); + TestSuccess("ge0://AwAAAAAAAA/Super%5FPoi", 0, 0, 4, "Super Poi"); + TestSuccess("ge0://AwAAAAAAAA/Super%5fPoi", 0, 0, 4, "Super Poi"); + TestSuccess("ge0://AwAAAAAAAA/Super Poi", 0, 0, 4, "Super_Poi"); + TestSuccess("ge0://AwAAAAAAAA/Super%20Poi", 0, 0, 4, "Super_Poi"); + TestSuccess("ge0://AwAAAAAAAA/Name%", 0, 0, 4, "Name"); + TestSuccess("ge0://AwAAAAAAAA/Name%2", 0, 0, 4, "Name"); + TestSuccess("ge0://AwAAAAAAAA/Hello%09World%0A", 0, 0, 4, "Hello\tWorld\n"); + TestSuccess("ge0://AwAAAAAAAA/Hello%%%%%%%%%", 0, 0, 4, "Hello"); + TestSuccess("ge0://AwAAAAAAAA/Hello%%%%%%%%%World", 0, 0, 4, "Hello"); + TestSuccess("ge0://AwAAAAAAAA/%d0%9c%d0%b8%d0%bd%d1%81%d0%ba_%d1%83%d0%bb._%d0%9b%d0%b5%d0%bd%d0%b8%d0%bd%d0%b0_9", 0, 0, 4, "Минск ул. Ленина 9"); + TestSuccess("ge0://AwAAAAAAAA/z%c3%bcrich_bahnhofstrasse", 0, 0, 4, "zürich bahnhofstrasse"); + TestSuccess("ge0://AwAAAAAAAA/%e5%8c%97%e4%ba%ac_or_B%c4%9bij%c4%abng%3F", 0, 0, 4, "北京 or Běijīng?"); + TestSuccess("ge0://AwAAAAAAAA/%d0%9a%d0%b0%d0%ba_%d0%b2%d1%8b_%d1%81%d1%87%d0%b8%d1%82%d0%b0%d0%b5%d1%82%d0%b5%2C_%d0%bd%d0%b0%d0%b4%d0%be_%d0%bb%d0%b8_%d0%bf%d0%b8%d1%81%d0%b0%d1%82%d1%8c_const_%d0%b4%d0%bb%d1%8f_%d0%bf%d0%b0%d1%80%d0%b0%d0%bc%d0%b5%d1%82%d1%80%d0%be%d0%b2%2C_%d0%ba%d0%be%d1%82%d0%be%d1%80%d1%8b%d0%b5_%d0%bf%d0%b5%d1%80%d0%b5%d0%b4%d0%b0%d1%8e%d1%82%d1%81%d1%8f_%d0%b2_%d1%84%d1%83%d0%bd%d0%ba%d1%86%d0%b8%d1%8e_%d0%bf%d0%be_%d0%b7%d0%bd%d0%b0%d1%87%d0%b5%d0%bd%d0%b8%d1%8e%3F", + 0, 0, 4, "Как вы считаете, надо ли писать const для параметров, которые передаются в функцию по значению?"); + TestSuccess("ge0://AwAAAAAAAA/\xd1\x81\xd1\x82\xd1\x80\xd0\xbe\xd0\xba\xd0\xb0_\xd0\xb2_\xd1\x8e\xd1\x82\xd1\x84-8", 0, 0, 4, "строка в ютф-8"); +} + +UNIT_TEST(LatLonFullAndClippedCoordinates) +{ + double maxLatDiffForCoordSize[10] = { 0 }; + double maxLonDiffForCoordSize[10] = { 0 }; + for (double lat = -90; lat <= 90; lat += 0.7) + { + for (double lon = -180; lon < 180; lon += 0.7) + { + char buf[20] = { 0 }; + MapsWithMe_GenShortShowMapUrl(lat, lon, 4, "", buf, ARRAY_SIZE(buf)); + for (int i = 9; i >= 1; --i) + { + string const str = string(buf).substr(7, i); + int const coordSize = str.size(); + Ge0ParserForTest parser; + double latTmp, lonTmp; + parser.DecodeLatLon(str, latTmp, lonTmp); + double const epsLat = GetLatEpsilon(coordSize); + double const epsLon = GetLonEpsilon(coordSize); + double const difLat = fabs(lat - latTmp); + double const difLon = fabs(lon - lonTmp); + TEST(difLat <= epsLat, (str, lat, latTmp, lon, lonTmp, difLat, epsLat)); + TEST(difLon <= epsLon, (str, lat, latTmp, lon, lonTmp, difLon, epsLon)); + maxLatDiffForCoordSize[coordSize] = max(maxLatDiffForCoordSize[coordSize], difLat); + maxLonDiffForCoordSize[coordSize] = max(maxLonDiffForCoordSize[coordSize], difLon); + } + } + } + + for (int coordSize = 1; coordSize <= 8; ++coordSize) + { + TEST(maxLatDiffForCoordSize[coordSize] > maxLatDiffForCoordSize[coordSize + 1], (coordSize)); + TEST(maxLonDiffForCoordSize[coordSize] > maxLonDiffForCoordSize[coordSize + 1], (coordSize)); + + TEST(maxLatDiffForCoordSize[coordSize] <= GetLatEpsilon(coordSize), (coordSize)); + TEST(maxLonDiffForCoordSize[coordSize] <= GetLonEpsilon(coordSize), (coordSize)); + + TEST(maxLatDiffForCoordSize[coordSize] > GetLatEpsilon(coordSize + 1), (coordSize)); + TEST(maxLonDiffForCoordSize[coordSize] > GetLonEpsilon(coordSize + 1), (coordSize)); + } +} + +UNIT_TEST(ClippedName) +{ + TestSuccess("ge0://AwAAAAAAAA/Super%5fPoi", 0, 0, 4, "Super Poi"); + TestSuccess("ge0://AwAAAAAAAA/Super%5fPo" , 0, 0, 4, "Super Po"); + TestSuccess("ge0://AwAAAAAAAA/Super%5fP" , 0, 0, 4, "Super P"); + TestSuccess("ge0://AwAAAAAAAA/Super%5f" , 0, 0, 4, "Super "); + TestSuccess("ge0://AwAAAAAAAA/Super%5" , 0, 0, 4, "Super"); + TestSuccess("ge0://AwAAAAAAAA/Super%" , 0, 0, 4, "Super"); + TestSuccess("ge0://AwAAAAAAAA/Super" , 0, 0, 4, "Super"); + TestSuccess("ge0://AwAAAAAAAA/Supe" , 0, 0, 4, "Supe"); + TestSuccess("ge0://AwAAAAAAAA/Sup" , 0, 0, 4, "Sup"); + TestSuccess("ge0://AwAAAAAAAA/Su" , 0, 0, 4, "Su"); + TestSuccess("ge0://AwAAAAAAAA/S" , 0, 0, 4, "S"); + TestSuccess("ge0://AwAAAAAAAA/" , 0, 0, 4, ""); + TestSuccess("ge0://AwAAAAAAAA" , 0, 0, 4, ""); +} diff --git a/map/map_tests/map_tests.pro b/map/map_tests/map_tests.pro index 4cb3026836..6ace6bad38 100644 --- a/map/map_tests/map_tests.pro +++ b/map/map_tests/map_tests.pro @@ -29,4 +29,5 @@ SOURCES += \ geourl_test.cpp \ measurement_tests.cpp \ mwm_url_tests.cpp \ - feature_processor_test.cpp + feature_processor_test.cpp \ + ge0_parser_tests.cpp \ diff --git a/map/url_api.hpp b/map/url_api.hpp new file mode 100644 index 0000000000..176c382dea --- /dev/null +++ b/map/url_api.hpp @@ -0,0 +1,28 @@ +#include "../std/vector.hpp" + +namespace url_api +{ + +struct Point +{ + Point() : m_lat(0), m_lon(0) {} + + double m_lat; + double m_lon; + string m_name; + string m_id; +}; + +struct Request +{ + vector m_points; + double m_viewportLat, m_viewportLon, m_viewportZoomLevel; + + void Clear() + { + m_points.clear(); + m_viewportLat = m_viewportLon = m_viewportZoomLevel = 0; + } +}; + +} // namespace url_api