diff --git a/indexer/map_object.hpp b/indexer/map_object.hpp index 61784124bc..7806fb3889 100644 --- a/indexer/map_object.hpp +++ b/indexer/map_object.hpp @@ -103,7 +103,7 @@ public: protected: /// @returns "the best" type to display in UI. std::string GetLocalizedType() const; - + FeatureID m_featureID; m2::PointD m_mercator; StringUtf8Multilang m_name; diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index c2250884fb..09772ed915 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -331,6 +331,7 @@ 34F73FA31E08300E00AC1FD6 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 34F73FA11E08300E00AC1FD6 /* Images.xcassets */; }; 34F742321E0834F400AC1FD6 /* UIViewController+Navigation.mm in Sources */ = {isa = PBXBuildFile; fileRef = 34F742301E0834F400AC1FD6 /* UIViewController+Navigation.mm */; }; 34FE5A6F1F18F30F00BCA729 /* TrafficButtonArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34FE5A6D1F18F30F00BCA729 /* TrafficButtonArea.swift */; }; + 3D15ACEE2155117000F725D5 /* MWMObjectsCategorySelectorDataSource.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3D15ACED2155117000F725D5 /* MWMObjectsCategorySelectorDataSource.mm */; }; 3D1958EB213804B6009A83EC /* libmetrics.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D1958EA213804B6009A83EC /* libmetrics.a */; }; 3DF9C22B207CC14A00DA0793 /* taxi_places in Resources */ = {isa = PBXBuildFile; fileRef = 3DF9C22A207CC14A00DA0793 /* taxi_places */; }; 408645FC21495EB1000A4A1D /* categories_cuisines.txt in Resources */ = {isa = PBXBuildFile; fileRef = 408645FB21495EB1000A4A1D /* categories_cuisines.txt */; }; @@ -1292,6 +1293,8 @@ 34FE4C431BCC013500066718 /* MWMMapWidgets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MWMMapWidgets.h; sourceTree = ""; }; 34FE4C441BCC013500066718 /* MWMMapWidgets.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MWMMapWidgets.mm; sourceTree = ""; }; 34FE5A6D1F18F30F00BCA729 /* TrafficButtonArea.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrafficButtonArea.swift; sourceTree = ""; }; + 3D15ACED2155117000F725D5 /* MWMObjectsCategorySelectorDataSource.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MWMObjectsCategorySelectorDataSource.mm; sourceTree = ""; }; + 3D15ACEF2155118800F725D5 /* MWMObjectsCategorySelectorDataSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMObjectsCategorySelectorDataSource.h; sourceTree = ""; }; 3D1958EA213804B6009A83EC /* libmetrics.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libmetrics.a; sourceTree = BUILT_PRODUCTS_DIR; }; 3DDB4BC31DAB98F000F4D021 /* libpartners_api.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libpartners_api.a; path = "../../../omim-xcode-build/Debug-iphonesimulator/libpartners_api.a"; sourceTree = ""; }; 3DF9C22A207CC14A00DA0793 /* taxi_places */ = {isa = PBXFileReference; lastKnownFileType = folder; name = taxi_places; path = ../../data/taxi_places; sourceTree = ""; }; @@ -3626,6 +3629,8 @@ F6E2FC5D1E097B9F0083EBEC /* OpeningHours */, F6E2FC821E097B9F0083EBEC /* Street */, 34763F0B1F30CCAC00F4D2D3 /* MWMEditorCellType.h */, + 3D15ACED2155117000F725D5 /* MWMObjectsCategorySelectorDataSource.mm */, + 3D15ACEF2155118800F725D5 /* MWMObjectsCategorySelectorDataSource.h */, ); path = Editor; sourceTree = ""; @@ -4634,6 +4639,7 @@ 34AB66471FC5AA330078E451 /* RouteManagerTableView.swift in Sources */, F6E2FE611E097BA00083EBEC /* MWMBookmarkCell.mm in Sources */, F6E2FEA31E097BA00083EBEC /* MWMPPView.mm in Sources */, + 3D15ACEE2155117000F725D5 /* MWMObjectsCategorySelectorDataSource.mm in Sources */, F69739B21FD197DB00FDA07D /* MWMDiscoveryTableManager.mm in Sources */, 3454D7D11E07F045004AF2AD /* UIImage+RGBAData.mm in Sources */, 6741A9B71BF340DE002C974C /* EAGLView.mm in Sources */, diff --git a/iphone/Maps/UI/Discovery/MWMDiscoveryController.mm b/iphone/Maps/UI/Discovery/MWMDiscoveryController.mm index 5928af2edc..68ae7015c8 100644 --- a/iphone/Maps/UI/Discovery/MWMDiscoveryController.mm +++ b/iphone/Maps/UI/Discovery/MWMDiscoveryController.mm @@ -22,6 +22,7 @@ #include "search/result.hpp" +#include "platform/localization.hpp" #include "platform/platform.hpp" #include "geometry/point2d.hpp" @@ -266,8 +267,12 @@ struct Callback auto getRoutePointInfo = ^(search::Result const & item) { point = item.GetFeatureCenter(); - title = @(item.GetString().c_str()); - subtitle = @(item.GetFeatureTypeName().c_str()); + + ASSERT(item.GetResultType() == search::Result::Type::Feature, ()); + auto const readableType = classif().GetReadableObjectName(item.GetFeatureType()); + + subtitle = @(platform::GetLocalizedTypeName(readableType).c_str()); + title = item.GetString().empty() ? subtitle : @(item.GetString().c_str()); }; switch (type) diff --git a/iphone/Maps/UI/Discovery/MWMDiscoveryTableManager.mm b/iphone/Maps/UI/Discovery/MWMDiscoveryTableManager.mm index 5a260795fb..df15203e9c 100644 --- a/iphone/Maps/UI/Discovery/MWMDiscoveryTableManager.mm +++ b/iphone/Maps/UI/Discovery/MWMDiscoveryTableManager.mm @@ -12,6 +12,7 @@ #include "search/result.hpp" +#include "platform/localization.hpp" #include "platform/measurement_utils.hpp" #include "geometry/distance_on_sphere.hpp" @@ -379,8 +380,12 @@ string GetDistance(m2::PointD const & from, m2::PointD const & to) auto const & pi = type == ItemType::Attractions ? model.GetAttractionProductInfoAt(indexPath.row) : model.GetCafeProductInfoAt(indexPath.row); tie(ratingValue, ratingType) = FormattedRating(pi.m_ugcRating); - [cell configWithTitle:@(sr.GetString().c_str()) - subtitle:@(sr.GetFeatureTypeName().c_str()) + + auto const readableType = classif().GetReadableObjectName(sr.GetFeatureType()); + auto const subtitle = platform::GetLocalizedTypeName(readableType); + + [cell configWithTitle:sr.GetString().empty() ? @(subtitle.c_str()) : @(sr.GetString().c_str()) + subtitle:@(subtitle.c_str()) distance:@(GetDistance(pt, sr.GetFeatureCenter()).c_str()) popular:sr.GetRankingInfo().m_popularity > 0 ratingValue:ratingValue @@ -461,7 +466,8 @@ string GetDistance(m2::PointD const & from, m2::PointD const & to) auto starsCount = sr.GetStarsCount(); if (starsCount == 0) { - subtitle = [@(sr.GetFeatureTypeName().c_str()) mutableCopy]; + auto const readableType = classif().GetReadableObjectName(sr.GetFeatureType()); + subtitle = [@(platform::GetLocalizedTypeName(readableType).c_str()) mutableCopy]; } else { @@ -473,7 +479,7 @@ string GetDistance(m2::PointD const & from, m2::PointD const & to) tie(ratingValue, ratingType) = FormattedRating(sr.GetHotelRating()); [cell configWithAvatarURL:nil - title:@(sr.GetString().c_str()) + title:sr.GetString().empty() ? [subtitle copy] : @(sr.GetString().c_str()) subtitle:[subtitle copy] price:@(sr.GetHotelApproximatePricing().c_str()) ratingValue:ratingValue diff --git a/iphone/Maps/UI/Editor/MWMEditorViewController.mm b/iphone/Maps/UI/Editor/MWMEditorViewController.mm index a63613bf55..f25e0a66c0 100644 --- a/iphone/Maps/UI/Editor/MWMEditorViewController.mm +++ b/iphone/Maps/UI/Editor/MWMEditorViewController.mm @@ -27,8 +27,11 @@ #include "editor/osm_editor.hpp" +#include "indexer/classificator.hpp" #include "indexer/feature_source.hpp" +#include "platform/localization.hpp" + namespace { NSString * const kAdditionalNamesEditorSegue = @"Editor2AdditionalNamesEditorSegue"; @@ -451,9 +454,12 @@ void registerCellsForTableView(vector const & cells, UITableV { case MWMEditorCellTypeCategory: { + auto types = m_mapObject.GetTypes(); + types.SortBySpec(); + auto const readableType = classif().GetReadableObjectName(*(types.begin())); MWMEditorCategoryCell * cCell = static_cast(cell); [cCell configureWithDelegate:self - detailTitle:@(m_mapObject.GetLocalizedType().c_str()) + detailTitle:@(platform::GetLocalizedTypeName(readableType).c_str()) isCreating:self.isCreating]; break; } @@ -1057,7 +1063,9 @@ void registerCellsForTableView(vector const & cells, UITableV @"are creating feature!"); MWMObjectsCategorySelectorController * dvc = segue.destinationViewController; dvc.delegate = self; - [dvc setSelectedCategory:m_mapObject.GetLocalizedType()]; + auto const type = *(m_mapObject.GetTypes().begin()); + auto const readableType = classif().GetReadableObjectName(type); + [dvc setSelectedCategory:readableType]; } else if ([segue.identifier isEqualToString:kAdditionalNamesEditorSegue]) { diff --git a/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorController.h b/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorController.h index 9d8c181309..50c68a9439 100644 --- a/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorController.h +++ b/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorController.h @@ -17,6 +17,6 @@ class EditableMapObject; @property (weak, nonatomic) id delegate; -- (void)setSelectedCategory:(string const &)category; +- (void)setSelectedCategory:(string const &)type; @end diff --git a/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorController.mm b/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorController.mm index 5cc3fb6c16..c784252fb3 100644 --- a/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorController.mm +++ b/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorController.mm @@ -1,5 +1,6 @@ #import "MWMObjectsCategorySelectorController.h" #import "MWMAuthorizationCommon.h" +#import "MWMObjectsCategorySelectorDataSource.h" #import "MWMCommon.h" #import "MWMEditorViewController.h" #import "MWMKeyboard.h" @@ -8,36 +9,26 @@ #import "SwiftBridge.h" #import "UIViewController+Navigation.h" -#include "LocaleTranslator.h" - #include "Framework.h" -#include "editor/new_feature_categories.hpp" - using namespace osm; namespace { NSString * const kToEditorSegue = @"CategorySelectorToEditorSegue"; - -string locale() -{ - return locale_translator::bcp47ToTwineLanguage(NSLocale.currentLocale.localeIdentifier); -} - } // namespace @interface MWMObjectsCategorySelectorController () { - NewFeatureCategories m_categories; - NewFeatureCategories::TNames m_filteredCategories; } @property(weak, nonatomic) IBOutlet UITableView * tableView; @property(weak, nonatomic) IBOutlet UISearchBar * searchBar; +@property(nonatomic) NSString * selectedType; @property(nonatomic) NSIndexPath * selectedIndexPath; @property(nonatomic) BOOL isSearch; +@property(nonatomic) MWMObjectsCategorySelectorDataSource * dataSource; @end @@ -46,11 +37,6 @@ string locale() - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; - if (self) - { - m_categories = GetFramework().GetEditorCategories(); - m_categories.AddLanguage(locale()); - } return self; } @@ -62,6 +48,12 @@ string locale() [self configNavBar]; [self configSearchBar]; [MWMKeyboard addObserver:self]; + self.dataSource = [[MWMObjectsCategorySelectorDataSource alloc] init]; + if (self.selectedType) + { + self.selectedIndexPath = + [NSIndexPath indexPathForRow:([self.dataSource getTypeIndex:self.selectedType])inSection:0]; + } } - (void)configTable @@ -72,14 +64,9 @@ string locale() forCellReuseIdentifier:[UITableViewCell className]]; } -- (void)setSelectedCategory:(string const &)category +- (void)setSelectedCategory:(string const &)type { - auto const & all = m_categories.GetAllCreatableTypeNames(); - auto const it = find_if( - all.begin(), all.end(), - [&category](NewFeatureCategories::TName const & name) { return name.first == category; }); - NSAssert(it != all.end(), @"Incorrect category!"); - self.selectedIndexPath = [NSIndexPath indexPathForRow:(distance(all.begin(), it)) inSection:0]; + self.selectedType = @(type.c_str()); } - (UIStatusBarStyle)preferredStatusBarStyle @@ -142,10 +129,11 @@ string locale() - (EditableMapObject)createdObject { - auto const & ds = [self dataSourceForSection:self.selectedIndexPath.section]; + auto const & typeName = [self.dataSource getType:self.selectedIndexPath.row].UTF8String; EditableMapObject emo; auto & f = GetFramework(); - if (!f.CreateMapObject(f.GetViewportCenter(), ds[self.selectedIndexPath.row].second, emo)) + auto const type = classif().GetTypeByReadableObjectName(typeName); + if (!f.CreateMapObject(f.GetViewportCenter(), type, emo)) NSAssert(false, @"This call should never fail, because IsPointCoveredByDownloadedMaps is " @"always called before!"); return emo; @@ -158,8 +146,7 @@ string locale() { auto cell = [tableView dequeueReusableCellWithCellClass:[UITableViewCell class] indexPath:indexPath]; - cell.textLabel.text = - @([self dataSourceForSection:indexPath.section][indexPath.row].first.c_str()); + cell.textLabel.text = [self.dataSource getTranslation:indexPath.row]; if ([indexPath isEqual:self.selectedIndexPath]) cell.accessoryType = UITableViewCellAccessoryCheckmark; else @@ -190,7 +177,7 @@ string locale() - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return [self dataSourceForSection:section].size(); + return [self.dataSource size]; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section @@ -205,33 +192,12 @@ string locale() // L(@"editor_add_select_category_all_subtitle"); } -- (NewFeatureCategories::TNames const &)dataSourceForSection:(NSInteger)section -{ - if (self.isSearch) - return m_filteredCategories; - return m_categories.GetAllCreatableTypeNames(); - // TODO(Vlad): Uncoment this line when we will be ready to show recent categories - // if (m_categories.m_lastUsed.empty()) - // return m_categories.m_allSorted; - // else - // return section == 0 ? m_categories.m_lastUsed : m_categories.m_allSorted; -} - #pragma mark - UISearchBarDelegate - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { - m_filteredCategories.clear(); - if (!searchText.length) - { - self.isSearch = NO; - [self.tableView reloadData]; - return; - } - - self.isSearch = YES; - string const query{[searchText lowercaseStringWithLocale:NSLocale.currentLocale].UTF8String}; - m_filteredCategories = m_categories.Search(query, locale()); + self.isSearch = searchText.length == 0 ? NO : YES; + [self.dataSource search:[searchText lowercaseStringWithLocale:NSLocale.currentLocale]]; [self.tableView reloadData]; } @@ -268,7 +234,7 @@ string locale() [searchBar setShowsCancelButton:isActiveState animated:YES]; [self.navigationController setNavigationBarHidden:isActiveState animated:YES]; if (!isActiveState) - m_filteredCategories.clear(); + [self.dataSource search:@""]; } @end diff --git a/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorDataSource.h b/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorDataSource.h new file mode 100644 index 0000000000..aebcce49dd --- /dev/null +++ b/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorDataSource.h @@ -0,0 +1,9 @@ +@interface MWMObjectsCategorySelectorDataSource : NSObject + +- (void)search:(NSString *)query; +- (NSString *)getTranslation:(NSInteger)row; +- (NSString *)getType:(NSInteger)row; +- (NSInteger)getTypeIndex:(NSString *)type; +- (NSInteger)size; + +@end diff --git a/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorDataSource.mm b/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorDataSource.mm new file mode 100644 index 0000000000..2360e45dfd --- /dev/null +++ b/iphone/Maps/UI/Editor/MWMObjectsCategorySelectorDataSource.mm @@ -0,0 +1,118 @@ +#import "MWMObjectsCategorySelectorDataSource.h" + +#include "LocaleTranslator.h" + +#include "Framework.h" + +#include "indexer/classificator.hpp" + +#include "platform/localization.hpp" + +#include "editor/new_feature_categories.hpp" + +#include +#include +#include +#include +#include + +using namespace osm; + +namespace +{ +using Category = std::pair; +using Categories = std::vector; + +std::string locale() +{ + return locale_translator::bcp47ToTwineLanguage(NSLocale.currentLocale.localeIdentifier); +} + +void SortByTranslation(Categories & result) +{ + std::sort(result.begin(), result.end(), [](Category const & lhs, Category const & rhs) + { + return lhs.first < rhs.first; + }); +} +} // namespace + +@interface MWMObjectsCategorySelectorDataSource() +{ + NewFeatureCategories m_categories; + Categories m_filteredCategories; + Categories m_allCategories; +} + +@end + +@implementation MWMObjectsCategorySelectorDataSource + +- (instancetype)init +{ + self = [super init]; + if (self) + [self load]; + + return self; +} + +- (void)load +{ + m_categories = GetFramework().GetEditorCategories(); + m_categories.AddLanguage(locale()); + auto const & types = m_categories.GetAllCreatableTypeNames(); + + m_allCategories.reserve(types.size()); + for (auto const & type : types) + m_allCategories.emplace_back(platform::GetLocalizedTypeName(type), type); + + SortByTranslation(m_allCategories); +} + +- (void)search:(NSString *)query +{ + m_filteredCategories.clear(); + + if (query.length == 0) + return; + + auto const types = m_categories.Search([query UTF8String]); + + m_filteredCategories.reserve(types.size()); + for (auto const & type : types) + m_filteredCategories.emplace_back(platform::GetLocalizedTypeName(type), type); + + SortByTranslation(m_filteredCategories); +} + +- (NSString *)getTranslation:(NSInteger)row +{ + return m_filteredCategories.empty() + ? @(m_allCategories[row].first.c_str()) : @(m_filteredCategories[row].first.c_str()); +} + +- (NSString *)getType:(NSInteger)row +{ + return m_filteredCategories.empty() + ? @(m_allCategories[row].second.c_str()) : @(m_filteredCategories[row].second.c_str()); +} + +- (NSInteger)getTypeIndex:(NSString *)type +{ + auto const it = find_if(m_allCategories.cbegin(), m_allCategories.cend(), + [type](Category const & item) + { + return type.UTF8String == item.second; + }); + + NSAssert(it != m_allCategories.cend(), @"Incorrect category!"); + return distance(m_allCategories.cbegin(), it); +} + +- (NSInteger)size +{ + return m_filteredCategories.empty() ? m_allCategories.size() : m_filteredCategories.size(); +} + +@end diff --git a/iphone/Maps/UI/Search/TableView/MWMSearchCell.h b/iphone/Maps/UI/Search/TableView/MWMSearchCell.h index 6eaff57e94..b118a4cfff 100644 --- a/iphone/Maps/UI/Search/TableView/MWMSearchCell.h +++ b/iphone/Maps/UI/Search/TableView/MWMSearchCell.h @@ -5,5 +5,5 @@ @interface MWMSearchCell : MWMTableViewCell - (void)config:(search::Result const &)result; - ++ (NSString *)getLocalizedTypeName:(search::Result const &)result; @end diff --git a/iphone/Maps/UI/Search/TableView/MWMSearchCell.mm b/iphone/Maps/UI/Search/TableView/MWMSearchCell.mm index f8a76f50c1..66291094a4 100644 --- a/iphone/Maps/UI/Search/TableView/MWMSearchCell.mm +++ b/iphone/Maps/UI/Search/TableView/MWMSearchCell.mm @@ -4,8 +4,14 @@ #include "Framework.h" +#include "indexer/classificator.hpp" + +#include "platform/localization.hpp" + #include "base/logging.hpp" +#include + @interface MWMSearchCell () @property (weak, nonatomic) IBOutlet UILabel * titleLabel; @@ -17,10 +23,9 @@ - (void)config:(search::Result const &)result { NSString * title = @(result.GetString().c_str()); - if (!title) + if (title.length == 0) { - self.titleLabel.text = @""; - return; + title = [self.class getLocalizedTypeName:result]; } NSDictionary * selectedTitleAttributes = [self selectedTitleAttributes]; NSDictionary * unselectedTitleAttributes = [self unselectedTitleAttributes]; @@ -55,6 +60,16 @@ self.backgroundColor = [UIColor white]; } ++ (NSString *)getLocalizedTypeName:(search::Result const &)result +{ + if (result.GetResultType() != search::Result::Type::Feature) + return @""; + + auto const readableType = classif().GetReadableObjectName(result.GetFeatureType()); + + return @(platform::GetLocalizedTypeName(readableType).c_str()); +} + - (NSDictionary *)selectedTitleAttributes { return nil; diff --git a/iphone/Maps/UI/Search/TableView/MWMSearchCommonCell.mm b/iphone/Maps/UI/Search/TableView/MWMSearchCommonCell.mm index a8d4b1b9a5..a142c9477e 100644 --- a/iphone/Maps/UI/Search/TableView/MWMSearchCommonCell.mm +++ b/iphone/Maps/UI/Search/TableView/MWMSearchCommonCell.mm @@ -49,8 +49,8 @@ bool PopularityHasHigherPriority(bool hasPosition, double distanceInMeters) productInfo:(search::ProductInfo const &)productInfo { [super config:result]; - self.typeLabel.text = @(result.GetFeatureTypeName().c_str()); + self.typeLabel.text = [self.class getLocalizedTypeName:result]; auto const hotelRating = result.GetHotelRating(); auto const ugcRating = productInfo.m_ugcRating; diff --git a/platform/localization.mm b/platform/localization.mm index cb9f90de87..c397ac1dbe 100644 --- a/platform/localization.mm +++ b/platform/localization.mm @@ -1,12 +1,15 @@ #include "platform/localization.hpp" +#include + +#import namespace platform { std::string GetLocalizedTypeName(std::string const & type) { - // TODO: Add code here. + auto key = "type." + type; + std::replace(key.begin(), key.end(), '-', '.'); - // Return type as is by default. - return type; + return [NSLocalizedString(@(key.c_str()), @"") UTF8String]; } } // namespace platform