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

22
libs/ge0/CMakeLists.txt Normal file
View file

@ -0,0 +1,22 @@
project(ge0)
set(SRC
geo_url_parser.cpp
geo_url_parser.hpp
parser.cpp
parser.hpp
url_generator.cpp
url_generator.hpp
)
omim_add_library(${PROJECT_NAME} ${SRC})
target_link_libraries(${PROJECT_NAME}
PUBLIC
coding
PRIVATE
base
geometry
)
omim_add_test_subdirectory(ge0_tests)

106
libs/ge0/ge0.php Normal file
View file

@ -0,0 +1,106 @@
<?php
// @TODO add tests
// Returns array(lat, lon, zoom) or empty array in the case of error
function DecodeGe0LatLonZoom($latLonZoom)
{
$OM_MAX_POINT_BYTES = 10;
$OM_MAX_COORD_BITS = $OM_MAX_POINT_BYTES * 3;
$FAILED = array();
$base64ReverseArray = GetBase64ReverseArray();
$zoom = $base64ReverseArray[ord($latLonZoom[0])];
if ($zoom > 63)
return $FAILED;
$zoom = $zoom / 4. + 4.;
$latLonStr = substr($latLonZoom, 1);
$latLonBytes = strlen($latLonStr);
$lat = 0;
$lon = 0;
for($i = 0, $shift = $OM_MAX_COORD_BITS - 3; $i < $latLonBytes; $i++, $shift -= 3)
{
$a = $base64ReverseArray[ord($latLonStr[$i])];
$lat1 = ((($a >> 5) & 1) << 2 |
(($a >> 3) & 1) << 1 |
(($a >> 1) & 1));
$lon1 = ((($a >> 4) & 1) << 2 |
(($a >> 2) & 1) << 1 |
($a & 1));
$lat |= $lat1 << $shift;
$lon |= $lon1 << $shift;
}
$middleOfSquare = 1 << (3 * ($OM_MAX_POINT_BYTES - $latLonBytes) - 1);
$lat += $middleOfSquare;
$lon += $middleOfSquare;
$lat = round($lat / ((1 << $OM_MAX_COORD_BITS) - 1) * 180.0 - 90.0, 5);
$lon = round($lon / (1 << $OM_MAX_COORD_BITS) * 360.0 - 180.0, 5);
if ($lat <= -90.0 || $lat >= 90.0)
return $FAILED;
if ($lon <= -180.0 || $lon >= 180.0)
return $FAILED;
return array($lat, $lon, $zoom);
}
// Returns decoded name
function DecodeGe0Name($name)
{
return str_replace('_', ' ', rawurldecode($name));
}
// Returns empty array in the case of error.
// In the good case, returns array(lat, lon, zoom) or array(lat, lon, zoom, name)
function DecodeGe0Url($url)
{
$OM_ZOOM_POSITION = 6;
$NAME_POSITON_IN_URL = 17;
$FAILED = array();
if (strlen($url) < 16 || strpos($url, "ge0://") != 0)
return $FAILED;
$base64ReverseArray = GetBase64ReverseArray();
$latLonZoom = DecodeGe0LatLonZoom(substr($url, 6, 10));
if (empty($latLonZoom))
return $FAILED;
if (strlen($url) < $NAME_POSITON_IN_URL)
return $latLonZoom;
$name = DecodeGe0Name(substr($url, $NAME_POSITON_IN_URL));
array_push($latLonZoom, $name);
return $latLonZoom;
}
// Internal helper function
function GetBase64ReverseArray()
{
static $base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
$base64ReverseArray = array();
// fill array with 255's
array_pad($base64ReverseArray, 256, 255);
for ($i = 0; $i < 64; $i++)
{
$c = $base64Alphabet[$i];
$base64ReverseArray[ord($c)] = $i;
}
return $base64ReverseArray;
}
//////////////////////////////////////
// Tests are below
//print_r(DecodeGe0Url("ge0://B4srhdHVVt/Some_Name"));
//print_r(DecodeGe0Url("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"));
?>

View file

@ -0,0 +1,11 @@
project(ge0_tests)
set(SRC
geo_url_tests.cpp
parser_tests.cpp
url_generator_tests.cpp
)
omim_add_test(${PROJECT_NAME} ${SRC})
target_link_libraries(${PROJECT_NAME} ge0)

View file

@ -0,0 +1,304 @@
#include "testing/testing.hpp"
#include "ge0/geo_url_parser.hpp"
namespace geo_url_tests
{
double const kEps = 1e-10;
using namespace geo;
UNIT_TEST(GeoUrl_Geo)
{
UnifiedParser parser;
GeoURLInfo info;
// Bare RFC5870 URI with lat,lon.
TEST(parser.Parse("geo:53.666,-27.666", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 53.666, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, -27.666, kEps, ());
// Bare RFC5870 URI with lat,lon,altitude.
TEST(parser.Parse("geo:53.666,-27.666,1000", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 53.666, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, -27.666, kEps, ());
// Altitude is ignored.
// OSMAnd on Android 2023-11-12.
TEST(parser.Parse("geo:35.34156,33.32210?z=16", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 35.34156, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 33.32210, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 16.0, kEps, ());
// Bare RFC5870 URI with a space between lat, lon.
TEST(parser.Parse("geo:53.666, -27.666", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 53.666, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, -27.666, kEps, ());
// Google's q= extension: 0,0 are treated as special values.
TEST(parser.Parse("geo:0,0?q=Kyrenia%20Castle&z=15", info), ());
TEST(!info.IsLatLonValid(), ());
TEST_EQUAL(info.m_query, "Kyrenia Castle", ());
TEST_EQUAL(info.m_zoom, 15, ());
// Coordinates in q= parameter.
TEST(parser.Parse("geo:0,0?z=14&q=-54.683486138,25.289361259", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, -54.683486138, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 25.289361259, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 14.0, kEps, ());
// Instagram on Android 2023-11-12.
TEST(parser.Parse("geo:?q=35.20488357543945, 33.345027923583984", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 35.20488357543945, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 33.345027923583984, kEps, ());
// Invalid lat,lon in q= parameter is saved as a query.
TEST(parser.Parse("geo:0,0?z=14&q=-100500.232,1213.232", info), ());
TEST(!info.IsLatLonValid(), ());
TEST_EQUAL(info.m_query, "-100500.232,1213.232", ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 14.0, kEps, ());
// RFC5870 additional parameters are ignored.
TEST(parser.Parse("geo:48.198634,16.371648;crs=wgs84;u=40", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 48.198634, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 16.371648, kEps, ());
// ;crs=wgs84;u=40 tail is ignored
// 0,0 are not treated as special values if q is empty.
TEST(parser.Parse("geo:0,0", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 0.0, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 0.0, kEps, ());
TEST_EQUAL(info.m_query, "", ());
// 0,0 are not treated as special values if q is empty.
TEST(parser.Parse("geo:0,0?q=", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 0.0, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 0.0, kEps, ());
TEST_EQUAL(info.m_query, "", ());
// Lat is 0.
TEST(parser.Parse("geo:0,16.371648", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 0.0, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 16.371648, kEps, ());
// Lon is 0.
TEST(parser.Parse("geo:53.666,0", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 53.666, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 0.0, kEps, ());
// URL Encoded comma (%2C) as delimiter
TEST(parser.Parse("geo:-18.9151863%2C-48.28712359999999?q=-18.9151863%2C-48.28712359999999", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, -18.9151863, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, -48.28712359999999, kEps, ());
// URL Encoded comma (%2C) and space (%20)
TEST(parser.Parse("geo:-18.9151863%2C%20-48.28712359999999?q=-18.9151863%2C-48.28712359999999", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, -18.9151863, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, -48.28712359999999, kEps, ());
// URL Encoded comma (%2C) and space as +
TEST(parser.Parse("geo:-18.9151863%2C+-48.28712359999999?q=-18.9151863%2C-48.28712359999999", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, -18.9151863, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, -48.28712359999999, kEps, ());
// URL encoded with altitude
TEST(parser.Parse("geo:53.666%2C-27.666%2C+1000", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 53.666, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, -27.666, kEps, ());
TEST(parser.Parse("geo:-32.899583,139.043969&z=12", info), ("& instead of ? from a user report"));
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, -32.899583, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 139.043969, kEps, ());
TEST_EQUAL(info.m_zoom, 12, ());
// Invalid coordinates.
TEST(!parser.Parse("geo:0,0garbage", info), ());
TEST(!parser.Parse("geo:garbage0,0", info), ());
TEST(!parser.Parse("geo:53.666", info), ());
// Garbage.
TEST(!parser.Parse("geo:", info), ());
TEST(!parser.Parse("geo:", info), ());
TEST(!parser.Parse("geo:random,garbage", info), ());
TEST(!parser.Parse("geo://random,garbage", info), ());
TEST(!parser.Parse("geo://point/?lon=27.666&lat=53.666&zoom=10", info), ());
}
UNIT_TEST(GeoURL_GeoLabel)
{
UnifiedParser parser;
GeoURLInfo info;
TEST(parser.Parse("geo:0,0?z=14&q=-54.683486138,25.289361259 (Forto%20dvaras)", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, -54.683486138, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 25.289361259, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 14.0, kEps, ());
TEST_EQUAL(info.m_label, "Forto dvaras", ());
TEST(parser.Parse("geo:0,0?z=14&q=-54.683486138,25.289361259(Forto%20dvaras)", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, -54.683486138, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 25.289361259, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 14.0, kEps, ());
TEST_EQUAL(info.m_label, "Forto dvaras", ());
TEST(parser.Parse("geo:0,0?q=-54.683486138,25.289361259&z=14 (Forto%20dvaras)", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, -54.683486138, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 25.289361259, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 14.0, kEps, ());
TEST_EQUAL(info.m_label, "Forto dvaras", ());
TEST(parser.Parse("geo:0,0(Forto%20dvaras)", info), ());
TEST(info.IsLatLonValid(), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 0.0, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 0.0, kEps, ());
TEST_EQUAL(info.m_label, "Forto dvaras", ());
TEST(parser.Parse("geo:0,0?q=Forto%20dvaras)", info), ());
TEST(!info.IsLatLonValid(), ());
TEST_EQUAL(info.m_query, "Forto dvaras)", ());
TEST(!parser.Parse("geo:(Forto%20dvaras)", info), ());
}
UNIT_TEST(GeoURL_GoogleMaps)
{
UnifiedParser parser;
GeoURLInfo info;
TEST(parser.Parse("https://maps.google.com/maps?z=16&q=Mezza9%401.3067198,103.83282", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 1.3067198, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 103.83282, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 16.0, kEps, ());
TEST(parser.Parse("https://maps.google.com/maps?z=16&q=House+of+Seafood+%40+180%40-1.356706,103.87591", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, -1.356706, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 103.87591, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 16.0, kEps, ());
TEST(parser.Parse("https://www.google.com/maps/place/Falafel+M.+Sahyoun/@33.8904447,35.5044618,16z", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 33.8904447, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 35.5044618, kEps, ());
// Sic: zoom is not parsed
// TEST_ALMOST_EQUAL_ABS(info.m_zoom, 16.0, kEps, ());
TEST(parser.Parse("https://www.google.com/maps?q=55.751809,-37.6130029", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 55.751809, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, -37.6130029, kEps, ());
}
UNIT_TEST(GeoURL_Yandex)
{
UnifiedParser parser;
GeoURLInfo info;
TEST(parser.Parse("https://yandex.ru/maps/213/moscow/?ll=37.000000%2C55.000000&z=10", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 55.0, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 37.0, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 10.0, kEps, ());
}
UNIT_TEST(GeoURL_2GIS)
{
UnifiedParser parser;
GeoURLInfo info;
TEST(parser.Parse("https://2gis.ru/moscow/firm/4504127908589159/center/37.6186,55.7601/zoom/15.9764", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 55.7601, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 37.6186, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 15.9764, kEps, ());
TEST(parser.Parse("https://2gis.ru/moscow/firm/4504127908589159/center/-37,55/zoom/15", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 55.0, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, -37.0, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 15.0, kEps, ());
TEST(parser.Parse("https://2gis.ru/moscow/firm/4504127908589159?m=37.618632%2C55.760069%2F15.232", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 55.760069, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 37.618632, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 15.232, kEps, ());
TEST(parser.Parse("https://2gis.ru/moscow/firm/4504127908589159?m=37.618632%2C-55.760069%2F15", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, -55.760069, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 37.618632, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 15.0, kEps, ());
}
UNIT_TEST(GeoURL_LatLon)
{
UnifiedParser parser;
GeoURLInfo info;
TEST(!parser.Parse("mapswithme:123.33,32.22/showmethemagic", info), ());
TEST(!parser.Parse("mapswithme:32.22, 123.33/showmethemagic", info), ());
TEST(!parser.Parse("model: iphone 7,1", info), ());
}
UNIT_TEST(GeoURL_OpenStreetMap)
{
UnifiedParser parser;
GeoURLInfo info;
TEST(parser.Parse("https://www.openstreetmap.org/#map=16/33.89041/35.50664", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 33.89041, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 35.50664, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 16.0, kEps, ());
TEST(parser.Parse("https://www.openstreetmap.org/search?query=Falafel%20Sahyoun#map=16/33.89041/35.50664", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 33.89041, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 35.50664, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 16.0, kEps, ());
TEST(parser.Parse("https://www.openstreetmap.org/#map=21/53.90323/-27.55806", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 53.90323, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, -27.55806, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 20.0, kEps, ());
TEST(parser.Parse("https://www.openstreetmap.org/way/45394171#map=10/34.67379/33.04422", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 34.67379, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 33.04422, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 10.0, kEps, ());
// Links in such format are generated by
// https://geohack.toolforge.org/geohack.php?pagename=White_House&params=38_53_52_N_77_02_11_W_type:landmark_region:US-DC
TEST(parser.Parse("https://www.openstreetmap.org/index.html?mlat=48.277222&mlon=24.152222&zoom=15", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 48.277222, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 24.152222, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 15.0, kEps, ());
}
UNIT_TEST(GeoURL_BadZoom)
{
UnifiedParser parser;
GeoURLInfo info;
TEST(parser.Parse("geo:52.23405,21.01547?z=22", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 52.23405, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 21.01547, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 20.0, kEps, ());
TEST(parser.Parse("geo:-52.23405,21.01547?z=nineteen", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, -52.23405, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 21.01547, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 0.0, kEps, ());
TEST(parser.Parse("geo:52.23405,21.01547?z=-1", info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 52.23405, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 21.01547, kEps, ());
TEST_GREATER_OR_EQUAL(info.m_zoom, 0.0, ());
}
} // namespace geo_url_tests

View file

@ -0,0 +1,314 @@
#include "testing/testing.hpp"
#include "ge0/parser.hpp"
#include "ge0/url_generator.hpp"
#include "base/math.hpp"
#include <algorithm>
#include <string>
using namespace std;
namespace
{
double const kZoomEps = 1e-10;
} // namespace
namespace ge0
{
class Ge0ParserForTest : public Ge0Parser
{
public:
using Ge0Parser::DecodeBase64Char;
using Ge0Parser::DecodeLatLon;
using Ge0Parser::DecodeZoom;
};
double GetLatEpsilon(size_t coordBytes)
{
// Should be / 2.0 but probably because of accumulates loss of precision, 1.77 works but 2.0
// doesn't.
double infelicity = 1 << ((kMaxPointBytes - coordBytes) * 3);
return infelicity / ((1 << kMaxCoordBits) - 1) * 180 / 1.77;
}
double GetLonEpsilon(size_t coordBytes)
{
// Should be / 2.0 but probably because of accumulates loss of precision, 1.77 works but 2.0
// doesn't.
double infelicity = 1 << ((kMaxPointBytes - coordBytes) * 3);
return (infelicity / ((1 << kMaxCoordBits) - 1)) * 360 / 1.77;
}
void TestSuccess(char const * s, double lat, double lon, double zoom, char const * name)
{
Ge0Parser parser;
Ge0Parser::Result parseResult;
bool const success = parser.Parse(s, parseResult);
TEST(success, (s, parseResult));
TEST_EQUAL(parseResult.m_name, string(name), (s));
double const latEps = GetLatEpsilon(9);
double const lonEps = GetLonEpsilon(9);
TEST_ALMOST_EQUAL_ABS(parseResult.m_lat, lat, latEps, (s, parseResult));
TEST_ALMOST_EQUAL_ABS(parseResult.m_lon, lon, lonEps, (s, parseResult));
TEST_ALMOST_EQUAL_ABS(parseResult.m_zoomLevel, zoom, kZoomEps, (s, parseResult));
}
void TestFailure(char const * s)
{
Ge0Parser parser;
Ge0Parser::Result parseResult;
bool const success = parser.Parse(s, parseResult);
TEST(!success, (s, parseResult));
}
bool ConvergenceTest(double lat, double lon, double latEps, double lonEps)
{
double tmpLat = lat;
double tmpLon = lon;
Ge0ParserForTest parser;
for (size_t i = 0; i < 100000; ++i)
{
char urlPrefix[] = "Coord6789";
ge0::LatLonToString(tmpLat, tmpLon, urlPrefix + 0, 9);
parser.DecodeLatLon(urlPrefix, tmpLat, tmpLon);
}
return AlmostEqualAbs(lat, tmpLat, latEps) && AlmostEqualAbs(lon, tmpLon, lonEps);
}
UNIT_TEST(Base64DecodingWorksForAValidChar)
{
Ge0ParserForTest parser;
for (int i = 0; i < 64; ++i)
{
char c = ge0::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<char>(i));
}
UNIT_TEST(Base64DecodingCharFrequency)
{
vector<int> charCounts(256, 0);
Ge0ParserForTest parser;
for (size_t i = 0; i < 256; ++i)
++charCounts[parser.DecodeBase64Char(static_cast<char>(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://Bzqqqqqqqq/Name", 45, 45, 4.25, "Name");
TestSuccess("ge0://BaF6F6F6F6/Name", -20, 20, 4.25, "Name");
TestSuccess("ge0://B4srhdHVVt/Name", 64.5234, 12.1234, 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");
TestFailure("ge0://Byqqqqqqq/Name");
TestFailure("ge0://Byqqqqqqq/");
TestFailure("ge0://Byqqqqqqq");
TestFailure("ge0://B");
TestFailure("ge0://");
}
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/"
"\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");
TestSuccess("ge0://AwAAAAAAAA/", 0, 0, 4, "");
TestSuccess("ge0://AwAAAAAAAA/s", 0, 0, 4, "s");
TestFailure("ge0://AwAAAAAAAAs");
TestFailure("ge0://AwAAAAAAAAss");
{
auto const name =
"Как вы считаете, надо ли писать const для параметров, которые передаются в функцию по "
"значению?";
double lat = 0;
double lon = 0;
double zoom = 4;
string const url =
"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";
Ge0Parser parser;
Ge0Parser::Result parseResult;
bool const success = parser.Parse(url.c_str(), parseResult);
TEST(success, (url, parseResult));
// Name would be valid but is too long.
TEST_NOT_EQUAL(parseResult.m_name, string(name), (url));
double const latEps = GetLatEpsilon(9);
double const lonEps = GetLonEpsilon(9);
TEST_ALMOST_EQUAL_ABS(parseResult.m_lat, lat, latEps, (url, parseResult));
TEST_ALMOST_EQUAL_ABS(parseResult.m_lon, lon, lonEps, (url, parseResult));
TEST_ALMOST_EQUAL_ABS(parseResult.m_zoomLevel, zoom, kZoomEps, (url, parseResult));
}
}
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)
{
string const buf = ge0::GenerateShortShowMapUrl(lat, lon, 4, "");
size_t const coordInd = buf.find("://") + 4;
for (int i = 9; i >= 1; --i)
{
string const str = buf.substr(coordInd, i);
size_t 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 (size_t 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, "");
}
UNIT_TEST(Bad_Base64)
{
TestSuccess("ge0://Byqqqqqqqq", 45, 0, 4.25, "");
TestFailure("ge0://Byqqqqqqq");
TestFailure("ge0://Byqqqqqqq\xEE");
}
UNIT_TEST(OtherPrefixes)
{
TestSuccess("http://comaps.app/Byqqqqqqqq/Name", 45, 0, 4.25, "Name");
TestSuccess("https://comaps.app/Byqqqqqqqq/Name", 45, 0, 4.25, "Name");
TestFailure("http://comapz.app/Byqqqqqqqq/Name");
TestSuccess("http://comaps.app/AwAAAAAAAA/Super%5fPoi", 0, 0, 4, "Super Poi");
TestSuccess("https://comaps.app/AwAAAAAAAA/Super%5fPoi", 0, 0, 4, "Super Poi");
TestFailure("https://comapz.app/AwAAAAAAAA/Super%5fPoi");
TestSuccess("https://comaps.app/Byqqqqqqqq", 45, 0, 4.25, "");
TestFailure("https://comaps.app/Byqqqqqqq");
}
} // namespace ge0

View file

@ -0,0 +1,362 @@
#include "testing/testing.hpp"
#include "ge0/geo_url_parser.hpp"
#include "ge0/url_generator.hpp"
#include <string>
using namespace std;
namespace
{
int const kTestCoordBytes = 9;
double const kEps = 1e-10;
} // namespace
namespace ge0
{
string TestLatLonToStr(double lat, double lon)
{
static char s[kTestCoordBytes + 1] = {0};
LatLonToString(lat, lon, s, kTestCoordBytes);
return string(s);
}
UNIT_TEST(Base64Char)
{
TEST_EQUAL('A', Base64Char(0), ());
TEST_EQUAL('B', Base64Char(1), ());
TEST_EQUAL('9', Base64Char(61), ());
TEST_EQUAL('-', Base64Char(62), ());
TEST_EQUAL('_', Base64Char(63), ());
}
UNIT_TEST(LatToInt_0)
{
TEST_EQUAL(499, LatToInt(0, 998), ());
TEST_EQUAL(500, LatToInt(0, 999), ());
TEST_EQUAL(500, LatToInt(0, 1000), ());
TEST_EQUAL(501, LatToInt(0, 1001), ());
}
UNIT_TEST(LatToInt_NearOrGreater90)
{
TEST_EQUAL(999, LatToInt(89.9, 1000), ());
TEST_EQUAL(1000, LatToInt(89.999999, 1000), ());
TEST_EQUAL(1000, LatToInt(90.0, 1000), ());
TEST_EQUAL(1000, LatToInt(90.1, 1000), ());
TEST_EQUAL(1000, LatToInt(100.0, 1000), ());
TEST_EQUAL(1000, LatToInt(180.0, 1000), ());
TEST_EQUAL(1000, LatToInt(350.0, 1000), ());
TEST_EQUAL(1000, LatToInt(360.0, 1000), ());
TEST_EQUAL(1000, LatToInt(370.0, 1000), ());
}
UNIT_TEST(LatToInt_NearOrLess_minus90)
{
TEST_EQUAL(1, LatToInt(-89.9, 1000), ());
TEST_EQUAL(0, LatToInt(-89.999999, 1000), ());
TEST_EQUAL(0, LatToInt(-90.0, 1000), ());
TEST_EQUAL(0, LatToInt(-90.1, 1000), ());
TEST_EQUAL(0, LatToInt(-100.0, 1000), ());
TEST_EQUAL(0, LatToInt(-180.0, 1000), ());
TEST_EQUAL(0, LatToInt(-350.0, 1000), ());
TEST_EQUAL(0, LatToInt(-360.0, 1000), ());
TEST_EQUAL(0, LatToInt(-370.0, 1000), ());
}
UNIT_TEST(LatToInt_NearOrLess_Rounding)
{
TEST_EQUAL(0, LatToInt(-90.0, 2), ());
TEST_EQUAL(0, LatToInt(-45.1, 2), ());
TEST_EQUAL(1, LatToInt(-45.0, 2), ());
TEST_EQUAL(1, LatToInt(0.0, 2), ());
TEST_EQUAL(1, LatToInt(44.9, 2), ());
TEST_EQUAL(2, LatToInt(45.0, 2), ());
TEST_EQUAL(2, LatToInt(90.0, 2), ());
}
UNIT_TEST(LonIn180180)
{
double const kEps = 1e-20;
TEST_ALMOST_EQUAL_ABS(0.0, LonIn180180(0), kEps, ());
TEST_ALMOST_EQUAL_ABS(20.0, LonIn180180(20), kEps, ());
TEST_ALMOST_EQUAL_ABS(90.0, LonIn180180(90), kEps, ());
TEST_ALMOST_EQUAL_ABS(179.0, LonIn180180(179), kEps, ());
TEST_ALMOST_EQUAL_ABS(-180.0, LonIn180180(180), kEps, ());
TEST_ALMOST_EQUAL_ABS(-180.0, LonIn180180(-180), kEps, ());
TEST_ALMOST_EQUAL_ABS(-179.0, LonIn180180(-179), kEps, ());
TEST_ALMOST_EQUAL_ABS(-20.0, LonIn180180(-20), kEps, ());
TEST_ALMOST_EQUAL_ABS(0.0, LonIn180180(360), kEps, ());
TEST_ALMOST_EQUAL_ABS(0.0, LonIn180180(720), kEps, ());
TEST_ALMOST_EQUAL_ABS(0.0, LonIn180180(-360), kEps, ());
TEST_ALMOST_EQUAL_ABS(0.0, LonIn180180(-720), kEps, ());
TEST_ALMOST_EQUAL_ABS(179.0, LonIn180180(360 + 179), kEps, ());
TEST_ALMOST_EQUAL_ABS(-180.0, LonIn180180(360 + 180), kEps, ());
TEST_ALMOST_EQUAL_ABS(-180.0, LonIn180180(360 - 180), kEps, ());
TEST_ALMOST_EQUAL_ABS(-179.0, LonIn180180(360 - 179), kEps, ());
}
UNIT_TEST(LonToInt_NearOrLess_Rounding)
{
/*
135 90 45
\ | /
03333
180 0\|/2
-----0-o-2---- 0
-180 0/|\2
11112
/ | \
-135 -90 -45
*/
TEST_EQUAL(0, LonToInt(-180.0, 3), ());
TEST_EQUAL(0, LonToInt(-135.1, 3), ());
TEST_EQUAL(1, LonToInt(-135.0, 3), ());
TEST_EQUAL(1, LonToInt(-90.0, 3), ());
TEST_EQUAL(1, LonToInt(-60.1, 3), ());
TEST_EQUAL(1, LonToInt(-45.1, 3), ());
TEST_EQUAL(2, LonToInt(-45.0, 3), ());
TEST_EQUAL(2, LonToInt(0.0, 3), ());
TEST_EQUAL(2, LonToInt(44.9, 3), ());
TEST_EQUAL(3, LonToInt(45.0, 3), ());
TEST_EQUAL(3, LonToInt(120.0, 3), ());
TEST_EQUAL(3, LonToInt(134.9, 3), ());
TEST_EQUAL(0, LonToInt(135.0, 3), ());
}
UNIT_TEST(LonToInt_0)
{
TEST_EQUAL(499, LonToInt(0, 997), ());
TEST_EQUAL(500, LonToInt(0, 998), ());
TEST_EQUAL(500, LonToInt(0, 999), ());
TEST_EQUAL(501, LonToInt(0, 1000), ());
TEST_EQUAL(501, LonToInt(0, 1001), ());
TEST_EQUAL(499, LonToInt(360, 997), ());
TEST_EQUAL(500, LonToInt(360, 998), ());
TEST_EQUAL(500, LonToInt(360, 999), ());
TEST_EQUAL(501, LonToInt(360, 1000), ());
TEST_EQUAL(501, LonToInt(360, 1001), ());
TEST_EQUAL(499, LonToInt(-360, 997), ());
TEST_EQUAL(500, LonToInt(-360, 998), ());
TEST_EQUAL(500, LonToInt(-360, 999), ());
TEST_EQUAL(501, LonToInt(-360, 1000), ());
TEST_EQUAL(501, LonToInt(-360, 1001), ());
}
UNIT_TEST(LonToInt_180)
{
TEST_EQUAL(0, LonToInt(-180, 1000), ());
TEST_EQUAL(0, LonToInt(180, 1000), ());
TEST_EQUAL(0, LonToInt(-180 - 360, 1000), ());
TEST_EQUAL(0, LonToInt(180 + 360, 1000), ());
}
UNIT_TEST(LonToInt_360)
{
TEST_EQUAL(2, LonToInt(0, 3), ());
TEST_EQUAL(2, LonToInt(0 + 360, 3), ());
TEST_EQUAL(2, LonToInt(0 - 360, 3), ());
TEST_EQUAL(2, LonToInt(1, 3), ());
TEST_EQUAL(2, LonToInt(1 + 360, 3), ());
TEST_EQUAL(2, LonToInt(1 - 360, 3), ());
TEST_EQUAL(2, LonToInt(-1, 3), ());
TEST_EQUAL(2, LonToInt(-1 + 360, 3), ());
TEST_EQUAL(2, LonToInt(-1 - 360, 3), ());
}
UNIT_TEST(LatLonToString)
{
TEST_EQUAL("AAAAAAAAA", TestLatLonToStr(-90, -180), ());
TEST_EQUAL("qqqqqqqqq", TestLatLonToStr(90, -180), ());
TEST_EQUAL("_________", TestLatLonToStr(90, 179.999999), ());
TEST_EQUAL("VVVVVVVVV", TestLatLonToStr(-90, 179.999999), ());
TEST_EQUAL("wAAAAAAAA", TestLatLonToStr(0.0, 0.0), ());
TEST_EQUAL("6qqqqqqqq", TestLatLonToStr(90.0, 0.0), ());
TEST_EQUAL("P________", TestLatLonToStr(-0.000001, -0.000001), ());
}
UNIT_TEST(LatLonToString_PrefixIsTheSame)
{
for (double lat = -95; lat <= 95; lat += 0.7)
{
for (double lon = -190; lon < 190; lon += 0.9)
{
char prevStepS[kMaxPointBytes + 1] = {0};
LatLonToString(lat, lon, prevStepS, kMaxPointBytes);
for (int len = kMaxPointBytes - 1; len > 0; --len)
{
// Test that the current string is a prefix of the previous one.
char s[kMaxPointBytes] = {0};
LatLonToString(lat, lon, s, len);
prevStepS[len] = 0;
TEST_EQUAL(s, string(prevStepS), ());
}
}
}
}
UNIT_TEST(LatLonToString_StringDensity)
{
int b64toI[256];
for (int i = 0; i < 256; ++i)
b64toI[i] = -1;
for (int i = 0; i < 64; ++i)
b64toI[static_cast<size_t>(Base64Char(i))] = i;
int num1[256] = {0};
int num2[256][256] = {{0}};
for (double lat = -90; lat <= 90; lat += 0.1)
{
for (double lon = -180; lon < 180; lon += 0.05)
{
char s[3] = {0};
LatLonToString(lat, lon, s, 2);
auto const s0 = static_cast<size_t>(s[0]);
auto const s1 = static_cast<size_t>(s[1]);
++num1[b64toI[s0]];
++num2[b64toI[s0]][b64toI[s1]];
}
}
int min1 = 1 << 30;
int min2 = 1 << 30;
int max1 = 0;
int max2 = 0;
for (int i = 0; i < 256; ++i)
{
if (num1[i] != 0 && num1[i] < min1)
min1 = num1[i];
if (num1[i] != 0 && num1[i] > max1)
max1 = num1[i];
for (int j = 0; j < 256; ++j)
{
if (num2[i][j] != 0 && num2[i][j] < min2)
min2 = num2[i][j];
if (num2[i][j] != 0 && num2[i][j] > max2)
max2 = num2[i][j];
}
}
// printf("\n1: %i-%i 2: %i-%i\n", min1, max1, min2, max2);
TEST((max1 - min1) * 1.0 / max1 < 0.05, ());
TEST((max2 - min2) * 1.0 / max2 < 0.05, ());
}
UNIT_TEST(GenerateShortShowMapUrl_SmokeTest)
{
string res = GenerateShortShowMapUrl(0, 0, 19, "Name");
TEST_EQUAL("cm://8wAAAAAAAA/Name", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_NameIsEmpty)
{
string res = GenerateShortShowMapUrl(0, 0, 19, "");
TEST_EQUAL("cm://8wAAAAAAAA", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_ZoomVerySmall)
{
string res = GenerateShortShowMapUrl(0, 0, 2, "Name");
TEST_EQUAL("cm://AwAAAAAAAA/Name", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_ZoomNegative)
{
string res = GenerateShortShowMapUrl(0, 0, -5, "Name");
TEST_EQUAL("cm://AwAAAAAAAA/Name", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_ZoomLarge)
{
string res = GenerateShortShowMapUrl(0, 0, 20, "Name");
TEST_EQUAL("cm://_wAAAAAAAA/Name", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_ZoomVeryLarge)
{
string res = GenerateShortShowMapUrl(0, 0, 2000000000, "Name");
TEST_EQUAL("cm://_wAAAAAAAA/Name", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_FractionalZoom)
{
string res = GenerateShortShowMapUrl(0, 0, 8.25, "Name");
TEST_EQUAL("cm://RwAAAAAAAA/Name", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_FractionalZoomRoundsDown)
{
string res = GenerateShortShowMapUrl(0, 0, 8.499, "Name");
TEST_EQUAL("cm://RwAAAAAAAA/Name", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_FractionalZoomNextStep)
{
string res = GenerateShortShowMapUrl(0, 0, 8.5, "Name");
TEST_EQUAL("cm://SwAAAAAAAA/Name", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_SpaceIsReplacedWithUnderscore)
{
string res = GenerateShortShowMapUrl(0, 0, 19, "Hello World");
TEST_EQUAL("cm://8wAAAAAAAA/Hello_World", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_NamesAreEscaped)
{
string res = GenerateShortShowMapUrl(0, 0, 19, "'Hello,World!%$");
TEST_EQUAL("cm://8wAAAAAAAA/%27Hello%2CWorld%21%25%24", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_UnderscoreIsReplacedWith_Percent_20)
{
string res = GenerateShortShowMapUrl(0, 0, 19, "Hello_World");
TEST_EQUAL("cm://8wAAAAAAAA/Hello%20World", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_ControlCharsAreEscaped)
{
string res = GenerateShortShowMapUrl(0, 0, 19, "Hello\tWorld\n");
TEST_EQUAL("cm://8wAAAAAAAA/Hello%09World%0A", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_Unicode)
{
string res = GenerateShortShowMapUrl(0, 0, 19, "\xe2\x98\x84");
TEST_EQUAL("cm://8wAAAAAAAA/\xe2\x98\x84", res, ());
}
UNIT_TEST(GenerateShortShowMapUrl_UnicodeMixedWithOtherChars)
{
string res = GenerateShortShowMapUrl(0, 0, 19, "Back_in \xe2\x98\x84!\xd1\x8e\xd0\xbc");
TEST_EQUAL("cm://8wAAAAAAAA/Back%20in_\xe2\x98\x84%21\xd1\x8e\xd0\xbc", res, ());
}
UNIT_TEST(GenerateGeoUri_SmokeTest)
{
string res = GenerateGeoUri(33.8904075, 35.5066454, 16.5, "Falafel M. Sahyoun");
TEST_EQUAL("geo:33.8904075,35.5066454?z=16.5&q=33.8904075,35.5066454(Falafel%20M.%20Sahyoun)", res, ());
// geo:33.8904075,35.5066454?z=16.5(Falafel%20M.%20Sahyoun)
// geo:33.890408,35.506645?z=16.5(Falafel%20M.%20Sahyoun)
geo::GeoURLInfo info;
geo::GeoParser parser;
TEST(parser.Parse(res, info), ());
TEST_ALMOST_EQUAL_ABS(info.m_lat, 33.8904075, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_lon, 35.5066454, kEps, ());
TEST_ALMOST_EQUAL_ABS(info.m_zoom, 16.5, kEps, ());
TEST_EQUAL(info.m_label, "Falafel M. Sahyoun", ());
}
} // namespace ge0

389
libs/ge0/geo_url_parser.cpp Normal file
View file

@ -0,0 +1,389 @@
#include "geo_url_parser.hpp"
#include "geometry/mercator.hpp"
#include "base/assert.hpp"
#include "base/logging.hpp"
#include "base/string_utils.hpp"
namespace geo
{
using namespace std;
namespace
{
double constexpr kEps = 1e-10;
// Same as scales::GetUpperScale() from indexer/scales.hpp.
// 1. Duplicated here to avoid a dependency on indexer/.
// 2. The value is arbitrary anyway: we want to parse a "z=%f" pair
// from a URL parameter, and different map providers may have different
// maximal zoom levels.
double constexpr kMaxZoom = 20.0;
bool MatchLatLonZoom(string const & s, boost::regex const & re, size_t lati, size_t loni, size_t zoomi, GeoURLInfo & info)
{
boost::smatch m;
if (!boost::regex_search(s, m, re) || m.size() != 4)
return false;
double lat, lon, zoom;
VERIFY(strings::to_double(m[lati].str(), lat), ());
VERIFY(strings::to_double(m[loni].str(), lon), ());
VERIFY(strings::to_double(m[zoomi].str(), zoom), ());
if (info.SetLat(lat) && info.SetLon(lon))
{
info.SetZoom(zoom);
return true;
}
LOG(LWARNING, ("Bad coordinates url:", s));
return false;
}
bool MatchHost(url::Url const & url, char const * host)
{
return url.GetHost().find(host) != std::string::npos;
}
// Canonical parsing is float-only coordinates and int-only scale.
std::string const kFloatCoord = R"(([+-]?\d+\.\d+))";
std::string const kIntScale = R"((\d+))";
// 2gis can accept float or int coordinates and scale.
std::string const kFloatIntCoord = R"(([+-]?\d+\.?\d*))";
std::string const kFloatIntScale = R"((\d+\.?\d*))";
} // namespace
LatLonParser::LatLonParser() : m_info(nullptr), m_regexp(boost::regex(kFloatCoord + ", *" + kFloatCoord)) {}
void LatLonParser::Reset(url::Url const & url, GeoURLInfo & info)
{
info.Reset();
m_info = &info;
m_swapLatLon = MatchHost(url, "2gis") || MatchHost(url, "yandex");
m_latPriority = m_lonPriority = -1;
}
bool LatLonParser::IsValid() const
{
return m_latPriority == m_lonPriority && m_latPriority != -1;
}
void LatLonParser::operator()(std::string name, std::string const & value)
{
strings::AsciiToLower(name);
if (name == "z" || name == "zoom")
{
double x;
if (strings::to_double(value, x))
m_info->SetZoom(x);
return;
}
int const priority = GetCoordinatesPriority(name);
if (priority == -1 || priority < m_latPriority || priority < m_lonPriority)
return;
if (priority != kXYPriority && priority != kLatLonPriority)
{
boost::smatch m;
if (boost::regex_search(value, m, m_regexp) && m.size() == 3)
{
double lat, lon;
VERIFY(strings::to_double(m[1].str(), lat), ());
VERIFY(strings::to_double(m[2].str(), lon), ());
if (m_swapLatLon)
std::swap(lat, lon);
if (m_info->SetLat(lat) && m_info->SetLon(lon))
{
m_latPriority = priority;
m_lonPriority = priority;
}
}
return;
}
double x;
if (strings::to_double(value, x))
{
if (name == "lat" || name == "mlat" || name == "y")
{
if (m_info->SetLat(x))
m_latPriority = priority;
}
else
{
ASSERT(name == "lon" || name == "mlon" || name == "x", (name));
if (m_info->SetLon(x))
m_lonPriority = priority;
}
}
}
int LatLonParser::GetCoordinatesPriority(string const & token)
{
if (token.empty())
return 0;
if (token == "q" || token == "m")
return 1;
if (token == "saddr" || token == "daddr")
return 2;
if (token == "sll")
return 3;
if (token.find("point") != string::npos)
return 4;
if (token == "ll")
return kLLPriority;
if (token == "x" || token == "y")
return kXYPriority;
if (token == "lat" || token == "mlat" || token == "lon" || token == "mlon")
return kLatLonPriority;
return -1;
}
GeoParser::GeoParser() : m_latlonRe(boost::regex(R"(([+-]?\d+(?:\.\d+)?), *([+-]?\d+(?:\.\d+)?)(:?, *([+-]?\d+(?:\.\d+)?))?)")), m_zoomRe(boost::regex(kFloatIntScale)) {}
bool GeoParser::Parse(std::string const & raw, GeoURLInfo & info) const
{
info.Reset();
/*
* References:
* - https://datatracker.ietf.org/doc/html/rfc5870
* - https://developer.android.com/guide/components/intents-common#Maps
* - https://developers.google.com/maps/documentation/urls/android-intents
*/
/*
* Check that URI starts with geo:
*/
if (!raw.starts_with("geo:"))
return false;
/*
* Check for trailing `(label)` which is not RFC3986-compliant (thanks, Google).
*/
size_t end = string::npos;
if (raw.size() > 2 && raw.back() == ')' && string::npos != (end = raw.rfind('(')))
{
// head (label)
// ^end
info.m_label = url::UrlDecode(raw.substr(end + 1, raw.size() - end - 2));
// Remove any whitespace between `head` and `(`.
end--;
while (end > 0 && (raw[end] == ' ' || raw[end] == '+'))
end--;
}
url::Url url(end == string::npos ? raw : raw.substr(0, end + 1));
if (!url.IsValid())
return false;
ASSERT_EQUAL(url.GetScheme(), "geo", ());
// Fix non-RFC url/hostname, reported by an Android user, with & instead of ?
std::string_view constexpr kWrongZoomInHost = "&z=";
if (std::string::npos != url.GetHost().find(kWrongZoomInHost))
{
auto fixedUrl = raw;
fixedUrl.replace(raw.find(kWrongZoomInHost), 1, 1, std::string::value_type{'?'});
url = url::Url{fixedUrl};
}
/*
* Parse coordinates before ';' character
*/
std::string coordinates = url.GetHost().substr(0, url.GetHost().find(';'));
if (!coordinates.empty())
{
boost::smatch m;
if (!boost::regex_match(coordinates, m, m_latlonRe) || m.size() < 3)
{
// no match? try URL decoding before giving up
coordinates = url::UrlDecode(coordinates);
if (!boost::regex_match(coordinates, m, m_latlonRe) || m.size() < 3)
{
LOG(LWARNING, ("Missing coordinates in", raw));
return false;
}
}
double lat, lon;
VERIFY(strings::to_double(m[1].str(), lat), ());
VERIFY(strings::to_double(m[2].str(), lon), ());
if (!mercator::ValidLat(lat) || !mercator::ValidLon(lon))
{
LOG(LWARNING, ("Invalid lat,lon in", raw));
return false;
}
info.m_lat = lat;
info.m_lon = lon;
}
/*
* Parse q=
*/
std::string const * q = url.GetParamValue("q");
if (q != nullptr && !q->empty())
{
// Try to extract lat,lon from q=
boost::smatch m;
if (boost::regex_match(*q, m, m_latlonRe) && m.size() != 3)
{
double lat, lon;
VERIFY(strings::to_double(m[1].str(), lat), ());
VERIFY(strings::to_double(m[2].str(), lon), ());
if (!mercator::ValidLat(lat) || !mercator::ValidLon(lon))
{
LOG(LWARNING, ("Invalid lat,lon after q=", raw));
info.m_query = *q;
}
else
{
info.m_lat = lat;
info.m_lon = lon;
}
}
else
{
info.m_query = *q;
}
// Ignore special 0,0 lat,lon if q= presents.
if (!info.m_query.empty() && fabs(info.m_lat) < kEps && fabs(info.m_lon) < kEps)
info.m_lat = info.m_lon = ms::LatLon::kInvalid;
}
if (!info.IsLatLonValid() && info.m_query.empty())
{
LOG(LWARNING, ("Missing coordinates and q=", raw));
return false;
}
/*
* Parse z=
*/
std::string const * z = url.GetParamValue("z");
if (z != nullptr)
{
boost::smatch m;
if (boost::regex_match(*z, m, m_zoomRe) && m.size() == 2)
{
double zoom;
VERIFY(strings::to_double(m[0].str(), zoom), ());
info.SetZoom(zoom);
}
else
{
LOG(LWARNING, ("Invalid z=", *z));
}
}
return true;
}
DoubleGISParser::DoubleGISParser()
: m_pathRe(boost::regex("/" + kFloatIntCoord + "," + kFloatIntCoord + "/zoom/" + kFloatIntScale))
, m_paramRe(boost::regex(kFloatIntCoord + "," + kFloatIntCoord + "/" + kFloatIntScale))
{}
bool DoubleGISParser::Parse(url::Url const & url, GeoURLInfo & info) const
{
info.Reset();
// Try m=$lon,$lat/$zoom first
auto const * value = url.GetParamValue("m");
if (value && MatchLatLonZoom(*value, m_paramRe, 2, 1, 3, info))
return true;
// Parse /$lon,$lat/zoom/$zoom from path next
return MatchLatLonZoom(url.GetHostAndPath(), m_pathRe, 2, 1, 3, info);
}
OpenStreetMapParser::OpenStreetMapParser() : m_regex(boost::regex(kIntScale + "/" + kFloatCoord + "/" + kFloatCoord)) {}
bool OpenStreetMapParser::Parse(url::Url const & url, GeoURLInfo & info) const
{
info.Reset();
auto const * mapV = url.GetParamValue("map");
return (mapV && MatchLatLonZoom(*mapV, m_regex, 2, 3, 1, info));
}
GeoURLInfo::GeoURLInfo()
{
Reset();
}
bool GeoURLInfo::IsLatLonValid() const
{
return m_lat != ms::LatLon::kInvalid && m_lon != ms::LatLon::kInvalid;
}
void GeoURLInfo::Reset()
{
m_lat = ms::LatLon::kInvalid;
m_lon = ms::LatLon::kInvalid;
m_zoom = 0.0;
m_query = "";
m_label = "";
}
void GeoURLInfo::SetZoom(double x)
{
if (x < 1.0)
LOG(LWARNING, ("Invalid zoom:", x));
else if (x > kMaxZoom)
m_zoom = kMaxZoom;
else
m_zoom = x;
}
bool GeoURLInfo::SetLat(double x)
{
if (mercator::ValidLat(x))
{
m_lat = x;
return true;
}
return false;
}
bool GeoURLInfo::SetLon(double x)
{
if (mercator::ValidLon(x))
{
m_lon = x;
return true;
}
return false;
}
bool UnifiedParser::Parse(std::string const & raw, GeoURLInfo & res)
{
if (raw.starts_with("geo:"))
return m_geoParser.Parse(raw, res);
url::Url url(raw);
if (!url.IsValid())
return false;
if (url.GetScheme() != "https" && url.GetScheme() != "http")
return false;
if (MatchHost(url, "2gis") && m_dgParser.Parse(url, res))
return true;
else if (MatchHost(url, "openstreetmap") && m_osmParser.Parse(url, res))
return true;
// Fall through.
m_llParser.Reset(url, res);
m_llParser({}, url.GetHostAndPath());
url.ForEachParam(m_llParser);
return m_llParser.IsValid();
}
} // namespace geo

105
libs/ge0/geo_url_parser.hpp Normal file
View file

@ -0,0 +1,105 @@
#pragma once
#include "coding/url.hpp"
#include <string>
#include <boost/regex.hpp>
namespace geo
{
class GeoURLInfo
{
public:
GeoURLInfo();
bool IsLatLonValid() const;
void Reset();
void SetZoom(double x);
bool SetLat(double x);
bool SetLon(double x);
double m_lat;
double m_lon;
double m_zoom;
std::string m_query;
std::string m_label;
};
class DoubleGISParser
{
public:
DoubleGISParser();
bool Parse(url::Url const & url, GeoURLInfo & info) const;
private:
boost::regex m_pathRe;
boost::regex m_paramRe;
};
class OpenStreetMapParser
{
public:
OpenStreetMapParser();
bool Parse(url::Url const & url, GeoURLInfo & info) const;
private:
boost::regex m_regex;
};
class LatLonParser
{
public:
LatLonParser();
void Reset(url::Url const & url, GeoURLInfo & info);
bool IsValid() const;
void operator()(std::string name, std::string const & value);
private:
// Usually (lat, lon), but some providers use (lon, lat).
static int constexpr kLLPriority = 5;
// We do not try to guess the projection and do not interpret (x, y)
// as Mercator coordinates in URLs. We simply use (y, x) for (lat, lon).
static int constexpr kXYPriority = 6;
static int constexpr kLatLonPriority = 7;
// Priority for accepting coordinates if we have many choices.
// -1 - not initialized
// 0 - coordinates in path;
// x - priority for query type (greater is better)
static int GetCoordinatesPriority(std::string const & token);
GeoURLInfo * m_info;
bool m_swapLatLon;
boost::regex m_regexp;
int m_latPriority;
int m_lonPriority;
};
class GeoParser
{
public:
GeoParser();
bool Parse(std::string const & url, GeoURLInfo & info) const;
private:
boost::regex m_latlonRe;
boost::regex m_zoomRe;
};
class UnifiedParser
{
public:
bool Parse(std::string const & url, GeoURLInfo & info);
private:
GeoParser m_geoParser;
DoubleGISParser m_dgParser;
OpenStreetMapParser m_osmParser;
LatLonParser m_llParser;
};
} // namespace geo

187
libs/ge0/parser.cpp Normal file
View file

@ -0,0 +1,187 @@
#include "ge0/parser.hpp"
#include "ge0/url_generator.hpp"
#include "coding/url.hpp"
#include "geometry/mercator.hpp"
#include "base/assert.hpp"
#include "base/math.hpp"
#include "base/string_utils.hpp"
#include <algorithm>
#include <sstream>
namespace ge0
{
Ge0Parser::Ge0Parser()
{
for (size_t i = 0; i < 256; ++i)
m_base64ReverseCharTable[i] = 255;
for (uint8_t i = 0; i < 64; ++i)
{
char c = Base64Char(i);
m_base64ReverseCharTable[static_cast<uint8_t>(c)] = i;
}
}
bool Ge0Parser::Parse(std::string const & url, Result & result)
{
// Original URL format:
//
// +------------------ 1 byte: zoom level
// |+-------+--------- 9 bytes: lat,lon
// || | +--+---- Variable number of bytes: point name
// || | | |
// cm://ZCoordba64/Name
// Alternative format (differs only in the prefix):
// http://comaps.app/ZCoordba64/Name
for (auto prefix : kGe0Prefixes)
if (url.starts_with(prefix))
return ParseAfterPrefix(url, prefix.size(), result);
return false;
}
bool Ge0Parser::ParseAfterPrefix(std::string const & url, size_t from, Result & result)
{
size_t constexpr kEncodedZoomAndCoordinatesLength = 10;
if (url.size() < from + kEncodedZoomAndCoordinatesLength)
return false;
size_t constexpr kMaxNameLength = 256;
size_t const posZoom = from;
size_t const posLatLon = posZoom + 1;
size_t const posName = from + kEncodedZoomAndCoordinatesLength + 1;
size_t const lengthLatLon = posName - posLatLon - 1;
uint8_t const zoomI = DecodeBase64Char(url[posZoom]);
if (zoomI >= 64)
return false;
result.m_zoomLevel = DecodeZoom(zoomI);
if (!DecodeLatLon(url.substr(posLatLon, lengthLatLon), result.m_lat, result.m_lon))
return false;
ASSERT(mercator::ValidLat(result.m_lat), (result.m_lat));
ASSERT(mercator::ValidLon(result.m_lon), (result.m_lon));
if (url.size() >= posName)
{
CHECK_GREATER(posName, 0, ());
if (url[posName - 1] != '/')
return false;
result.m_name = DecodeName(url.substr(posName, std::min(url.size() - posName, kMaxNameLength)));
}
return true;
}
uint8_t Ge0Parser::DecodeBase64Char(char const c)
{
return m_base64ReverseCharTable[static_cast<uint8_t>(c)];
}
double Ge0Parser::DecodeZoom(uint8_t const zoomByte)
{
// Coding zoom - int newZoom = ((oldZoom - 4) * 4)
return static_cast<double>(zoomByte) / 4 + 4;
}
bool Ge0Parser::DecodeLatLon(std::string const & s, double & lat, double & lon)
{
int latInt = 0;
int lonInt = 0;
if (!DecodeLatLonToInt(s, latInt, lonInt))
return false;
lat = DecodeLatFromInt(latInt, (1 << kMaxCoordBits) - 1);
lon = DecodeLonFromInt(lonInt, (1 << kMaxCoordBits) - 1);
return true;
}
bool Ge0Parser::DecodeLatLonToInt(std::string const & s, int & lat, int & lon)
{
int shift = kMaxCoordBits - 3;
for (size_t i = 0; i < s.size(); ++i, shift -= 3)
{
uint8_t const a = DecodeBase64Char(s[i]);
if (a >= 64)
return false;
int const lat1 = (((a >> 5) & 1) << 2 | ((a >> 3) & 1) << 1 | ((a >> 1) & 1));
int const lon1 = (((a >> 4) & 1) << 2 | ((a >> 2) & 1) << 1 | (a & 1));
lat |= lat1 << shift;
lon |= lon1 << shift;
}
double const middleOfSquare = 1 << (3 * (kMaxPointBytes - s.size()) - 1);
lat += middleOfSquare;
lon += middleOfSquare;
return true;
}
double Ge0Parser::DecodeLatFromInt(int const lat, int const maxValue)
{
return static_cast<double>(lat) / maxValue * 180 - 90;
}
double Ge0Parser::DecodeLonFromInt(int const lon, int const maxValue)
{
return static_cast<double>(lon) / (maxValue + 1.0) * 360.0 - 180;
}
std::string Ge0Parser::DecodeName(std::string name)
{
ValidateName(name);
name = url::UrlDecode(name);
SpacesToUnderscore(name);
return name;
}
void Ge0Parser::SpacesToUnderscore(std::string & name)
{
for (size_t i = 0; i < name.size(); ++i)
if (name[i] == ' ')
name[i] = '_';
else if (name[i] == '_')
name[i] = ' ';
}
void Ge0Parser::ValidateName(std::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 Ge0Parser::IsHexChar(char const a)
{
return ((a >= '0' && a <= '9') || (a >= 'A' && a <= 'F') || (a >= 'a' && a <= 'f'));
}
std::string DebugPrint(Ge0Parser::Result const & r)
{
std::ostringstream oss;
oss << "ParseResult [";
oss << "zoom=" << r.m_zoomLevel << ", ";
oss << "lat=" << r.m_lat << ", ";
oss << "lon=" << r.m_lon << ", ";
oss << "name=" << r.m_name << "]";
return oss.str();
}
} // namespace ge0

47
libs/ge0/parser.hpp Normal file
View file

@ -0,0 +1,47 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
namespace ge0
{
class Ge0Parser
{
public:
// Used by map/mwm_url.cpp.
static constexpr std::array<std::string_view, 6> kGe0Prefixes = {
{"https://comaps.at/", "cm://", "http://comaps.at/", "ge0://", "http://ge0.me/", "https://ge0.me/"}};
struct Result
{
double m_zoomLevel = 0.0;
double m_lat = 0.0;
double m_lon = 0.0;
std::string m_name;
};
Ge0Parser();
bool Parse(std::string const & url, Result & result);
bool ParseAfterPrefix(std::string const & url, size_t from, Result & result);
protected:
uint8_t DecodeBase64Char(char const c);
static double DecodeZoom(uint8_t const zoomByte);
bool DecodeLatLon(std::string const & s, double & lat, double & lon);
bool DecodeLatLonToInt(std::string const & s, int & lat, int & lon);
double DecodeLatFromInt(int const lat, int const maxValue);
double DecodeLonFromInt(int const lon, int const maxValue);
std::string DecodeName(std::string name);
void SpacesToUnderscore(std::string & name);
void ValidateName(std::string & name);
static bool IsHexChar(char const a);
private:
uint8_t m_base64ReverseCharTable[256];
};
std::string DebugPrint(Ge0Parser::Result const & r);
} // namespace ge0

203
libs/ge0/url_generator.cpp Normal file
View file

@ -0,0 +1,203 @@
#include "ge0/url_generator.hpp"
#include "base/assert.hpp"
#include "base/math.hpp"
#include "coding/url.hpp"
#include <iomanip>
#include <sstream>
namespace
{
// Replaces ' ' with '_' and vice versa.
std::string TransformName(std::string const & s)
{
std::string result = s;
for (auto & c : result)
if (c == ' ')
c = '_';
else if (c == '_')
c = ' ';
return result;
}
// URL-encodes string |s|.
// URL restricted / unsafe / unwise characters are %-encoded.
// See rfc3986, rfc1738, rfc2396.
//
// Not compatible with the url encode function from coding/.
std::string UrlEncodeString(std::string const & s)
{
std::string result;
result.reserve(s.size() * 3 + 1);
for (size_t i = 0; i < s.size(); ++i)
{
auto const c = static_cast<unsigned char>(s[i]);
switch (c)
{
case 0x00:
case 0x01:
case 0x02:
case 0x03:
case 0x04:
case 0x05:
case 0x06:
case 0x07:
case 0x08:
case 0x09:
case 0x0A:
case 0x0B:
case 0x0C:
case 0x0D:
case 0x0E:
case 0x0F:
case 0x10:
case 0x11:
case 0x12:
case 0x13:
case 0x14:
case 0x15:
case 0x16:
case 0x17:
case 0x18:
case 0x19:
case 0x1A:
case 0x1B:
case 0x1C:
case 0x1D:
case 0x1E:
case 0x1F:
case 0x7F:
case ' ':
case '<':
case '>':
case '#':
case '%':
case '"':
case '!':
case '*':
case '\'':
case '(':
case ')':
case ';':
case ':':
case '@':
case '&':
case '=':
case '+':
case '$':
case ',':
case '/':
case '?':
case '[':
case ']':
case '{':
case '}':
case '|':
case '^':
case '`':
result += '%';
result += "0123456789ABCDEF"[c >> 4];
result += "0123456789ABCDEF"[c & 15];
break;
default: result += s[i];
}
}
return result;
}
} // namespace
namespace ge0
{
std::string GenerateShortShowMapUrl(double lat, double lon, double zoom, std::string const & name)
{
size_t constexpr schemaLength = 5; // strlen("cm://")
std::string urlSample = "cm://ZCoordba64";
int const zoomI = (zoom <= 4 ? 0 : (zoom >= 19.75 ? 63 : static_cast<int>((zoom - 4) * 4)));
urlSample[schemaLength] = Base64Char(zoomI);
LatLonToString(lat, lon, urlSample.data() + schemaLength + 1, 9);
if (!name.empty())
{
urlSample += '/';
urlSample += UrlEncodeString(TransformName(name));
}
return urlSample;
}
std::string GenerateGeoUri(double lat, double lon, double zoom, std::string const & name)
{
std::ostringstream oss;
oss << "geo:" << std::fixed << std::setprecision(7) << lat << ',' << lon << "?z=" << std::setprecision(1) << zoom;
// For Google Maps compatibility, otherwise it doesn't select the point on the map, only shows the area.
oss << "&q=" << std::setprecision(7) << lat << ',' << lon;
if (!name.empty())
oss << '(' << url::UrlEncode(name) << ')';
return oss.str();
}
char Base64Char(int x)
{
CHECK_GREATER_OR_EQUAL(x, 0, ());
CHECK_LESS(x, 64, ());
return "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"[x];
}
// Map latitude: [-90, 90] -> [0, maxValue]
int LatToInt(double lat, int maxValue)
{
// M = maxValue, L = maxValue-1
// lat: -90 90
// x: 0 1 2 L M
// |--+--|--+--|--...--|--+--|
// 000111111222222...LLLLLMMMM
double const x = (lat + 90.0) / 180.0 * maxValue;
return x < 0 ? 0 : (x > maxValue ? maxValue : math::iround(x));
}
// Make lon in [-180, 180)
double LonIn180180(double lon)
{
if (lon >= 0)
return fmod(lon + 180.0, 360.0) - 180.0;
// Handle the case of l = -180
double const l = fmod(lon - 180.0, 360.0) + 180.0;
return l < 180.0 ? l : l - 360.0;
}
// Map longitude: [-180, 180) -> [0, maxValue]
int LonToInt(double lon, int maxValue)
{
double const x = (LonIn180180(lon) + 180.0) / 360.0 * (maxValue + 1.0) + 0.5;
return (x <= 0 || x >= maxValue + 1) ? 0 : static_cast<int>(x);
}
void LatLonToString(double lat, double lon, char * s, size_t nBytes)
{
if (nBytes > kMaxPointBytes)
nBytes = kMaxPointBytes;
int const latI = LatToInt(lat, (1 << kMaxCoordBits) - 1);
int const lonI = LonToInt(lon, (1 << kMaxCoordBits) - 1);
size_t i;
int shift;
for (i = 0, shift = kMaxCoordBits - 3; i < nBytes; ++i, shift -= 3)
{
int const latBits = latI >> shift & 7;
int const lonBits = lonI >> shift & 7;
int const nextByte = (latBits >> 2 & 1) << 5 | (lonBits >> 2 & 1) << 4 | (latBits >> 1 & 1) << 3 |
(lonBits >> 1 & 1) << 2 | (latBits & 1) << 1 | (lonBits & 1);
s[i] = Base64Char(nextByte);
}
}
} // namespace ge0

View file

@ -0,0 +1,45 @@
#pragma once
#include <string>
namespace ge0
{
// Max number of base64 bytes to encode a geo point.
inline static int constexpr kMaxPointBytes = 10;
inline static int constexpr kMaxCoordBits = kMaxPointBytes * 3;
// Generates a short url.
//
// URL format:
//
// +------------------ 1 byte: zoom level
// |+-------+--------- 9 bytes: lat,lon
// || | +--+---- Variable number of bytes: point name
// || | | |
// cm://ZCoordba64/Name
std::string GenerateShortShowMapUrl(double lat, double lon, double zoomLevel, std::string const & name);
// Generates a geo: uri.
//
// - https://datatracker.ietf.org/doc/html/rfc5870
// - https://developer.android.com/guide/components/intents-common#Maps
// - https://developers.google.com/maps/documentation/urls/android-intents
//
// URL format:
//
// +-------------------------------- lat
// | +-------------------- lon
// | | +---- zoom
// | | | +-- url-encoded name
// | | | |
// | | | |
// geo:54.683486138,25.289361259&z=14(Forto%20dvaras)
std::string GenerateGeoUri(double lat, double lon, double zoom, std::string const & name);
// Exposed for testing.
char Base64Char(int x);
int LatToInt(double lat, int maxValue);
double LonIn180180(double lon);
int LonToInt(double lon, int maxValue);
void LatLonToString(double lat, double lon, char * s, size_t nBytes);
} // namespace ge0