diff --git a/data/banners.txt b/data/banners.txt new file mode 100644 index 0000000000..f202ac1ed6 --- /dev/null +++ b/data/banners.txt @@ -0,0 +1,10 @@ +[deliveryclub_ru] +start = 2016-12-01 +end = 2017-01-15 +icon = +url = http://fas.st/VtumFw +aux = test + +[lamoda_ua] +start = 2016-12-01 +end = 2017-01-15 diff --git a/defines.hpp b/defines.hpp index 356b33d190..ff4d6e5338 100644 --- a/defines.hpp +++ b/defines.hpp @@ -79,5 +79,6 @@ #define REPLACED_TAGS_FILE "replaced_tags.txt" #define MIXED_TAGS_FILE "mixed_tags.txt" +#define BANNERS_FILE "banners.txt" #define LOCALIZATION_DESCRIPTION_SUFFIX " Description" diff --git a/docs/CPP_STYLE.md b/docs/CPP_STYLE.md index 87bf2c28b5..0f9582abca 100644 --- a/docs/CPP_STYLE.md +++ b/docs/CPP_STYLE.md @@ -9,6 +9,8 @@ Below are our specific (but not all!) exceptions to the Google's coding standard - File names are lowercase with underscores, like `file_reader.cpp`. - We use `#pragma once` instead of the `#define` Guard in header files. - We don't include system, std and boost headers directly, use `#include "std/"`. +- Includes are sorted and grouped by directory, there should be newlines between different directories. +- Order of directories in includes: "current_dir/current_file.hpp", other includes from the same dir, includes from other dirs sorted by dependencies: `coding, geometry, base, std, "defines.hpp", 3party` should go last in that order. - We ARE using C++ exceptions. - We are using all features of C++11 (the only known exception is thread_local which is not fully supported on all platforms). - We don't use boost libraries which require linking (and prefer C++11 types over their boost counterparts). diff --git a/generator/osm2meta.hpp b/generator/osm2meta.hpp index f187a769f8..d455aa8ed2 100644 --- a/generator/osm2meta.hpp +++ b/generator/osm2meta.hpp @@ -97,6 +97,7 @@ public: case Metadata::FMD_SPONSORED_ID: valid = ValidateAndFormat_sponsored_id(v); break; case Metadata::FMD_PRICE_RATE: valid = ValidateAndFormat_price_rate(v); break; case Metadata::FMD_RATING: valid = ValidateAndFormat_rating(v); break; + case Metadata::FMD_BANNER_URL: valid = ValidateAndFormat_url(v); break; case Metadata::FMD_TEST_ID: case Metadata::FMD_COUNT: CHECK(false, ("FMD_COUNT can not be used as a type.")); diff --git a/indexer/CMakeLists.txt b/indexer/CMakeLists.txt index e239721de0..3ae176faf9 100644 --- a/indexer/CMakeLists.txt +++ b/indexer/CMakeLists.txt @@ -6,6 +6,8 @@ set( SRC altitude_loader.cpp altitude_loader.hpp + banners.cpp + banners.hpp categories_holder_loader.cpp categories_holder.cpp categories_holder.hpp diff --git a/indexer/banners.cpp b/indexer/banners.cpp new file mode 100644 index 0000000000..509d069f4b --- /dev/null +++ b/indexer/banners.cpp @@ -0,0 +1,224 @@ +#include "indexer/banners.hpp" +#include "indexer/classificator.hpp" +#include "indexer/feature.hpp" + +#include "platform/platform.hpp" + +#include "coding/reader_streambuf.hpp" + +#include "base/stl_add.hpp" +#include "base/string_utils.hpp" + +#include "defines.hpp" + +namespace +{ +time_t constexpr kEternity = 0; + +// Convert ISO date to unix time. +bool StringToTimestamp(string const & s, time_t & result) +{ + istringstream is(s); + tm time; + is >> get_time(&time, "%F"); + CHECK(!is.fail(), ("Wrong date format:", s, "(expecting YYYY-MM-DD)")); + + time.tm_sec = time.tm_min = time.tm_hour = 0; + + time_t timestamp = mktime(&time); + if (timestamp < 0) + return false; + + result = timestamp; + return true; +} +} // namespace + +namespace banner +{ +string Banner::GetProperty(string const & name) const +{ + if (name == "lang") + { + return "ru"; // TODO(@zverik): this variable, {mwmlang}, {country} etc. + } + else + { + auto const property = m_properties.find(name); + if (property != m_properties.end()) + return property->second; + } + return {}; +} + +Banner::Banner(string const & id) : m_id(id) +{ + m_messageBase = "banner_" + id; + m_iconName = m_messageBase + ".png"; + m_defaultUrl = ""; + m_activeAfter = time(nullptr); + m_activeBefore = kEternity; +} + +bool Banner::IsActive() const +{ + if (IsEmpty()) + return false; + time_t const now = time(nullptr); + return now >= m_activeAfter && (m_activeBefore == kEternity || now < m_activeBefore); +} + +void Banner::SetProperty(string const & name, string const & value) +{ + if (name == "messages") + { + m_messageBase = value; + } + else if (name == "icon") + { + m_iconName = value; + } + else if (name == "url") + { + CHECK(strings::StartsWith(value, "http://") || strings::StartsWith(value, "https://"), + ("URL without a protocol for banner", m_id)); + m_defaultUrl = value; + } + else if (name == "start") + { + CHECK(StringToTimestamp(value, m_activeAfter), ("Wrong start date", value, "for banner", m_id)); + } + else if (name == "end") + { + CHECK(StringToTimestamp(value, m_activeBefore), ("Wrong end date", value, "for banner", m_id)); + m_activeBefore += 24 * 60 * 60; // Add a day so we don't miss one + } + else + { + m_properties.emplace(make_pair(name, value)); + } +} + +string Banner::GetFormattedUrl(string const & url) const +{ + string baseUrl = url.empty() ? m_defaultUrl : url; + auto start = baseUrl.find('{'); + while (start != string::npos) + { + auto end = baseUrl.find('}', start + 1); + if (end == string::npos) + break; + string value = GetProperty(baseUrl.substr(start + 1, end - start - 1)); + if (!value.empty()) + { + baseUrl.replace(start, end - start + 1, value); + end -= end - start + 1 - value.length(); + } + start = baseUrl.find('{', end + 1); + } + return baseUrl; +} + +void BannerSet::ReadBanners(istream & s) +{ + m_banners.clear(); + + Banner banner; + string type; + int lineNumber = 1; + for (string line; getline(s, line); ++lineNumber) + { + strings::Trim(line); + if (line.empty() || line.front() == '#') + continue; + + auto const equals = line.find('='); + if (equals == string::npos) + { + // Section header, should be in square brackets. + CHECK(line.front() == '[' && line.back() == ']', ("Unknown syntax at line", lineNumber)); + strings::Trim(line, " \t[]"); + CHECK(!line.empty(), ("Empty banner ID at line", lineNumber)); + if (!banner.IsEmpty()) + Add(banner, type); + banner = Banner(line); + type = "sponsored-banner-" + line; + } + else + { + // Variable definition, must be inside a section. + CHECK(!banner.IsEmpty(), ("Variable definition outside a section at line", lineNumber)); + string name = line.substr(0, equals); + string value = line.substr(equals + 1); + strings::Trim(name); + CHECK(!name.empty(), ("Empty variable name at line", lineNumber)); + strings::Trim(value); + if (name == "type") + type = value; + else + banner.SetProperty(name, value); + } + } + if (!banner.IsEmpty()) + Add(banner, type); +} + +void BannerSet::Add(Banner const & banner, string const & type) +{ + vector v; + strings::Tokenize(type, "-", MakeBackInsertFunctor(v)); + uint32_t const ctype = classif().GetTypeByPathSafe(v); + if (ctype == 0) + { + LOG(LWARNING, ("Missing type", type, "for a banner")); + } + else + { + CHECK(m_banners.find(ctype) == m_banners.end(), ("Duplicate banner type", type)); + m_banners.emplace(make_pair(ctype, banner)); + } +} + +bool BannerSet::HasBannerForType(uint32_t type) const +{ + return m_banners.find(type) != m_banners.end(); +} + +Banner const & BannerSet::GetBannerForType(uint32_t type) const +{ + auto const result = m_banners.find(type); + CHECK(result != m_banners.end(), ("GetBannerForType() for absent banner")); + return result->second; +} + +bool BannerSet::HasBannerForFeature(FeatureType const & ft) const +{ + bool result = false; + ft.ForEachType([this, &result](uint32_t type) + { + if (!result && HasBannerForType(type)) + result = true; + }); + return result; +} + +Banner const & BannerSet::GetBannerForFeature(FeatureType const & ft) const +{ + vector types; + ft.ForEachType([this, &types](uint32_t type) + { + if (types.empty() && HasBannerForType(type)) + types.push_back(type); + }); + CHECK(!types.empty(), ("No banners for the feature", ft)); + return GetBannerForType(types.front()); +} + +void BannerSet::LoadBanners() +{ + auto reader = GetPlatform().GetReader(BANNERS_FILE); + ReaderStreamBuf buffer(move(reader)); + istream s(&buffer); + ReadBanners(s); +} +} // namespace banner diff --git a/indexer/banners.hpp b/indexer/banners.hpp new file mode 100644 index 0000000000..9fb280e49a --- /dev/null +++ b/indexer/banners.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "std/ctime.hpp" +#include "std/iostream.hpp" +#include "std/string.hpp" +#include "std/unordered_map.hpp" + +class FeatureType; + +namespace banner +{ +class Banner +{ +public: + Banner() = default; + explicit Banner(string const & id); + + bool IsEmpty() const { return m_id.empty(); } + string GetMessageBase() const { return m_messageBase; } + string GetIconName() const { return m_iconName; } + string GetDefaultUrl() const { return m_defaultUrl; } + bool IsActive() const; + + /// Replaces inline variables in the URL, uses the default banner URL if url is not specified. + string GetFormattedUrl(string const & url = {}) const; + + /// Usually called from BannerSet. + void SetProperty(string const & name, string const & value); + +private: + string m_id; + string m_messageBase; + string m_iconName; + string m_defaultUrl; + time_t m_activeAfter; + time_t m_activeBefore; + unordered_map m_properties; + + string GetProperty(string const & name) const; +}; + +class BannerSet +{ +public: + void LoadBanners(); + void ReadBanners(istream & s); + + bool HasBannerForType(uint32_t type) const; + Banner const & GetBannerForType(uint32_t type) const; + + bool HasBannerForFeature(FeatureType const & ft) const; + Banner const & GetBannerForFeature(FeatureType const & ft) const; + +private: + unordered_map m_banners; + + void Add(Banner const & banner, string const & type); +}; +} diff --git a/indexer/feature_meta.cpp b/indexer/feature_meta.cpp index 73e9451b2d..c1d34820fa 100644 --- a/indexer/feature_meta.cpp +++ b/indexer/feature_meta.cpp @@ -94,6 +94,8 @@ bool Metadata::TypeFromString(string const & k, Metadata::EType & outType) outType = Metadata::FMD_PRICE_RATE; else if (k == "rating:sponsored") outType = Metadata::FMD_RATING; + else if (k == "banner_url") + outType = Metadata::FMD_BANNER_URL; else return false; @@ -176,6 +178,7 @@ string DebugPrint(feature::Metadata::EType type) case Metadata::FMD_SPONSORED_ID: return "ref:sponsored"; case Metadata::FMD_PRICE_RATE: return "price_rate"; case Metadata::FMD_RATING: return "rating:sponsored"; + case Metadata::FMD_BANNER_URL: return "banner_url"; case Metadata::FMD_TEST_ID: return "test_id"; case Metadata::FMD_COUNT: CHECK(false, ("FMD_COUNT can not be used as a type.")); }; diff --git a/indexer/feature_meta.hpp b/indexer/feature_meta.hpp index 85cede6dc5..9b2a6862bd 100644 --- a/indexer/feature_meta.hpp +++ b/indexer/feature_meta.hpp @@ -126,6 +126,7 @@ public: FMD_SPONSORED_ID = 24, FMD_PRICE_RATE = 25, FMD_RATING = 26, + FMD_BANNER_URL = 27, FMD_COUNT }; diff --git a/indexer/indexer.pro b/indexer/indexer.pro index 57a5db455a..9b68c979fb 100644 --- a/indexer/indexer.pro +++ b/indexer/indexer.pro @@ -11,6 +11,7 @@ include($$ROOT_DIR/common.pri) SOURCES += \ altitude_loader.cpp \ + banners.cpp \ categories_holder.cpp \ categories_holder_loader.cpp \ categories_index.cpp \ @@ -64,6 +65,7 @@ SOURCES += \ HEADERS += \ altitude_loader.hpp \ + banners.hpp \ categories_holder.hpp \ categories_index.hpp \ cell_coverer.hpp \ diff --git a/indexer/indexer_tests/banners_test.cpp b/indexer/indexer_tests/banners_test.cpp new file mode 100644 index 0000000000..8fc773eeac --- /dev/null +++ b/indexer/indexer_tests/banners_test.cpp @@ -0,0 +1,70 @@ +#include "testing/testing.hpp" + +#include "indexer/banners.hpp" +#include "indexer/classificator.hpp" +#include "indexer/classificator_loader.hpp" + +#include "std/iostream.hpp" + +using namespace banner; + +UNIT_TEST(Banners_Load) +{ + char const kBanners[] = + "# comment\n" + "[abc]\n" + "icon = test.png\n" + "start=2016-07-14\n" + "type= shop-clothes \n" + "\n" + "[error]\n" + "[re_123]\n" + "type=shop-shoes\n" + "url=http://{aux}.com\n" + " \t aux=\t\ttest \n" + "[future]\n" + "type=shop-wine\n" + "start=2028-01-01\n" + "end=2028-12-31\n" + "[final]\n" + "type=shop-pet\n" + "start=2016-07-13\n" + "end=2016-07-14\n" + "\t"; + + classificator::Load(); + Classificator & c = classif(); + + BannerSet bs; + istringstream is(kBanners); + bs.ReadBanners(is); + + TEST(bs.HasBannerForType(c.GetTypeByPath({"shop", "clothes"})), ()); + Banner const & bannerAbc = bs.GetBannerForType(c.GetTypeByPath({"shop", "clothes"})); + TEST(!bannerAbc.IsEmpty(), ()); + TEST_EQUAL(bannerAbc.GetIconName(), "test.png", ()); + TEST_EQUAL(bannerAbc.GetMessageBase(), "banner_abc", ()); + TEST_EQUAL(bannerAbc.GetDefaultUrl(), "", ()); + TEST_EQUAL(bannerAbc.GetFormattedUrl("http://example.com"), "http://example.com", ()); + TEST_EQUAL(bannerAbc.GetFormattedUrl(), "", ()); + TEST(bannerAbc.IsActive(), ()); + + TEST(bs.HasBannerForType(c.GetTypeByPath({"shop", "shoes"})), ()); + Banner const & bannerRe = bs.GetBannerForType(c.GetTypeByPath({"shop", "shoes"})); + TEST(!bannerRe.IsEmpty(), ()); + TEST(bannerRe.IsActive(), ()); + TEST_EQUAL(bannerRe.GetIconName(), "banner_re_123.png", ()); + TEST_EQUAL(bannerRe.GetFormattedUrl(), "http://test.com", ()); + TEST_EQUAL(bannerRe.GetFormattedUrl("http://ex.ru/{aux}?var={v}"), "http://ex.ru/test?var={v}", ()); + + TEST(bs.HasBannerForType(c.GetTypeByPath({"shop", "wine"})), ()); + Banner const & bannerFuture = bs.GetBannerForType(c.GetTypeByPath({"shop", "wine"})); + TEST(!bannerFuture.IsEmpty(), ()); + TEST(!bannerFuture.IsActive(), ()); + + TEST(bs.HasBannerForType(c.GetTypeByPath({"shop", "pet"})), ()); + Banner const & bannerFinal = bs.GetBannerForType(c.GetTypeByPath({"shop", "pet"})); + TEST(!bannerFinal.IsEmpty(), ()); + TEST(!bannerFinal.IsActive(), ()); + TEST_EQUAL(bannerFinal.GetFormattedUrl("http://{aux}.ru"), "http://{aux}.ru", ()); +} diff --git a/indexer/indexer_tests/indexer_tests.pro b/indexer/indexer_tests/indexer_tests.pro index 1ff165f0ac..79b5de4b4a 100644 --- a/indexer/indexer_tests/indexer_tests.pro +++ b/indexer/indexer_tests/indexer_tests.pro @@ -28,6 +28,7 @@ HEADERS += \ SOURCES += \ ../../testing/testingmain.cpp \ + banners_test.cpp \ categories_test.cpp \ cell_coverer_test.cpp \ cell_id_test.cpp \ diff --git a/indexer/map_object.hpp b/indexer/map_object.hpp index e296e668b9..23c408b6ab 100644 --- a/indexer/map_object.hpp +++ b/indexer/map_object.hpp @@ -146,6 +146,7 @@ vector MetadataToProps(vector const & metadata) case Metadata::FMD_SPONSORED_ID: case Metadata::FMD_PRICE_RATE: case Metadata::FMD_RATING: + case Metadata::FMD_BANNER_URL: case Metadata::FMD_TEST_ID: case Metadata::FMD_COUNT: break; diff --git a/map/framework.cpp b/map/framework.cpp index 7c70e76dcd..85446f0d11 100644 --- a/map/framework.cpp +++ b/map/framework.cpp @@ -387,6 +387,8 @@ Framework::Framework() m_displayedCategories = make_unique(GetDefaultCategories()); + m_bannerSet.LoadBanners(); + // To avoid possible races - init country info getter once in constructor. InitCountryInfoGetter(); LOG(LDEBUG, ("Country info getter initialized")); @@ -844,6 +846,8 @@ void Framework::FillInfoFromFeatureType(FeatureType const & ft, place_page::Info info.m_sponsoredDescriptionUrl = url; } + if (m_bannerSet.HasBannerForFeature(ft)) + info.m_banner = m_bannerSet.GetBannerForFeature(ft); info.m_canEditOrAdd = featureStatus != osm::Editor::FeatureStatus::Obsolete && CanEditMap() && !info.IsSponsored(); diff --git a/map/framework.hpp b/map/framework.hpp index 7c4850dfc6..e2b965113b 100644 --- a/map/framework.hpp +++ b/map/framework.hpp @@ -18,10 +18,11 @@ #include "drape/oglcontextfactory.hpp" +#include "indexer/banners.hpp" #include "indexer/data_header.hpp" +#include "indexer/index_helpers.hpp" #include "indexer/map_style.hpp" #include "indexer/new_feature_categories.hpp" -#include "indexer/index_helpers.hpp" #include "editor/user_stats.hpp" @@ -168,6 +169,8 @@ protected: unique_ptr m_bookingApi = make_unique(); unique_ptr m_uberApi = make_unique(); + banner::BannerSet m_bannerSet; + df::DrapeApi m_drapeApi; bool m_isRenderingEnabled; diff --git a/map/place_page_info.cpp b/map/place_page_info.cpp index 0c0c46dc4b..d843e7a671 100644 --- a/map/place_page_info.cpp +++ b/map/place_page_info.cpp @@ -38,12 +38,7 @@ bool Info::HasWifi() const { return GetInternet() == osm::Internet::Wlan; } bool Info::HasBanner() const { - // Dummy implementation. - // auto const now = time(nullptr); - // auto const bannerStartTime = strings::to_uint(m_metadata.Get(feature::Metadata::FMD_BANNER_FROM)); - // auto const bannerEndTime = strings::to_uint(m_metadata.Get(feature::Metadata::FMD_BANNER_TO)); - // return !(now < bannerStartTime || now > bannerEndTime); - return true; + return !m_banner.IsEmpty() && m_banner.IsActive(); } string Info::FormatNewBookmarkName() const @@ -174,30 +169,26 @@ string Info::GetApproximatePricing() const string Info::GetBannerTitleId() const { - // Dummy implementation. - //return m_metadata.Get(feature::Metadata::FMD_BANNER_ID) + "title"; - return "title"; + CHECK(!m_banner.IsEmpty(), ()); + return m_banner.GetMessageBase() + "_title"; } string Info::GetBannerMessageId() const { - // Dummy implementation. - //return m_metadata.Get(feature::Metadata::FMD_BANNER_ID) + "message"; - return "message"; + CHECK(!m_banner.IsEmpty(), ()); + return m_banner.GetMessageBase() + "_message"; } string Info::GetBannerIconId() const { - // Dummy implementation. - //return m_metadata.Get(feature::Metadata::FMD_BANNER_ID) + "icon"; - return "icon"; + CHECK(!m_banner.IsEmpty(), ()); + return m_banner.GetIconName(); } string Info::GetBannerUrl() const { - // Dummy implementation. - //return m_metadata.Get(feature::Metadata::FMD_BANNER_URL); - return "url"; + CHECK(!m_banner.IsEmpty(), ()); + return m_banner.GetFormattedUrl(m_metadata.Get(feature::Metadata::FMD_BANNER_URL)); } void Info::SetMercator(m2::PointD const & mercator) { m_mercator = mercator; } diff --git a/map/place_page_info.hpp b/map/place_page_info.hpp index 81cdb82dbd..a7ef979169 100644 --- a/map/place_page_info.hpp +++ b/map/place_page_info.hpp @@ -4,6 +4,7 @@ #include "storage/index.hpp" +#include "indexer/banners.hpp" #include "indexer/feature_data.hpp" #include "indexer/feature_meta.hpp" #include "indexer/map_object.hpp" @@ -111,6 +112,9 @@ public: string m_sponsoredUrl; string m_sponsoredDescriptionUrl; + /// A banner associated with the object. + banner::Banner m_banner; + /// Which country this MapObject is in. /// For a country point it will be set to topmost node for country. storage::TCountryId m_countryId = storage::kInvalidCountryId;