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

View file

@ -0,0 +1,7 @@
#import "MWMSearch.h"
@interface MWMSearch (CoreSpotlight)
+ (void)addCategoriesToSpotlight;
@end

View file

@ -0,0 +1,78 @@
#import <CoreSpotlight/CoreSpotlight.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import <CoreApi/Framework.h>
#import <CoreApi/AppInfo.h>
#import <CoreApi/MWMCommon.h>
#import "MWMSearch+CoreSpotlight.h"
#import "MWMSettings.h"
@implementation MWMSearch (CoreSpotlight)
+ (void)addCategoriesToSpotlight
{
if (isIOSVersionLessThan(9) || ![CSSearchableIndex isIndexingAvailable])
return;
NSString * localeLanguageId = [[AppInfo sharedInfo] languageId];
if ([localeLanguageId isEqualToString:[MWMSettings spotlightLocaleLanguageId]])
return;
auto const & categories = GetFramework().GetDisplayedCategories();
auto const & categoriesKeys = categories.GetKeys();
NSMutableArray<CSSearchableItem *> * items = [@[] mutableCopy];
for (auto const & categoryKey : categoriesKeys)
{
CSSearchableItemAttributeSet * attrSet = [[CSSearchableItemAttributeSet alloc]
initWithItemContentType: UTTypeItem.identifier];
NSString * categoryName = nil;
NSMutableDictionary<NSString *, NSString *> * localizedStrings = [@{} mutableCopy];
categories.ForEachSynonym(categoryKey, [&localizedStrings, &localeLanguageId, &categoryName](
std::string const & name, std::string const & locale) {
NSString * nsName = @(name.c_str());
NSString * nsLocale = @(locale.c_str());
if ([localeLanguageId isEqualToString:nsLocale])
categoryName = nsName;
localizedStrings[nsLocale] = nsName;
});
attrSet.alternateNames = localizedStrings.allValues;
attrSet.keywords = localizedStrings.allValues;
attrSet.title = categoryName;
attrSet.displayName = [[CSLocalizedString alloc] initWithLocalizedStrings:localizedStrings];
NSString * categoryKeyString = @(categoryKey.c_str());
NSString * imageName = [NSString stringWithFormat:@"Search/Categories/%@", [categoryKeyString stringByReplacingOccurrencesOfString: @"category_" withString:@""]];
UIImage * image = [UIImage imageNamed:imageName inBundle:nil compatibleWithTraitCollection:[UITraitCollection traitCollectionWithUserInterfaceStyle: UIUserInterfaceStyleLight]];
UIGraphicsBeginImageContext(CGSizeMake(360, 360));
[image drawInRect:CGRectMake(0, 0, 360, 360)];
UIImage * resizedImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext() ;
attrSet.thumbnailData = UIImagePNGRepresentation(resizedImage);
CSSearchableItem * item =
[[CSSearchableItem alloc] initWithUniqueIdentifier:categoryKeyString
domainIdentifier:@"comaps.app.categories"
attributeSet:attrSet];
[items addObject:item];
}
[[CSSearchableIndex defaultSearchableIndex]
indexSearchableItems:items
completionHandler:^(NSError * _Nullable error) {
if (error)
{
NSError * err = error;
LOG(LERROR,
("addCategoriesToSpotlight failed: ", err.localizedDescription.UTF8String));
}
else
{
LOG(LINFO, ("addCategoriesToSpotlight succeded"));
[MWMSettings setSpotlightLocaleLanguageId:localeLanguageId];
}
}];
}
@end

View file

@ -0,0 +1,58 @@
#import "MWMSearchObserver.h"
#import "SearchItemType.h"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, SearchTextSource) {
SearchTextSourceTypedText,
SearchTextSourceCategory,
SearchTextSourceHistory,
SearchTextSourceSuggestion,
SearchTextSourceDeeplink
};
typedef NS_ENUM(NSUInteger, SearchMode) {
SearchModeEverywhere,
SearchModeViewport,
SearchModeEverywhereAndViewport
};
@class SearchResult;
@class SearchQuery;
@protocol SearchManager
+ (void)addObserver:(id<MWMSearchObserver>)observer;
+ (void)removeObserver:(id<MWMSearchObserver>)observer;
+ (void)saveQuery:(SearchQuery *)query;
+ (void)searchQuery:(SearchQuery *)query;
+ (void)showResultAtIndex:(NSUInteger)index;
+ (SearchMode)searchMode;
+ (void)setSearchMode:(SearchMode)mode;
+ (NSArray<SearchResult *> *)getResults;
+ (void)clear;
@end
NS_SWIFT_NAME(Search)
@interface MWMSearch : NSObject<SearchManager>
+ (SearchItemType)resultTypeWithRow:(NSUInteger)row;
+ (NSUInteger)containerIndexWithRow:(NSUInteger)row;
+ (SearchResult *)resultWithContainerIndex:(NSUInteger)index;
+ (NSUInteger)suggestionsCount;
+ (NSUInteger)resultsCount;
- (instancetype)init __attribute__((unavailable("call +manager instead")));
- (instancetype)copy __attribute__((unavailable("call +manager instead")));
- (instancetype)copyWithZone:(NSZone *)zone __attribute__((unavailable("call +manager instead")));
+ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable("call +manager instead")));
+ (instancetype)new __attribute__((unavailable("call +manager instead")));
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,297 @@
#import "MWMSearch.h"
#import "MWMFrameworkListener.h"
#import "MWMFrameworkObservers.h"
#import "SearchResult+Core.h"
#import "SwiftBridge.h"
#include <CoreApi/MWMTypes.h>
#include <CoreApi/Framework.h>
#include "platform/network_policy.hpp"
namespace {
using Observer = id<MWMSearchObserver>;
using Observers = NSHashTable<Observer>;
} // namespace
@interface MWMSearch () <MWMFrameworkDrapeObserver>
@property(nonatomic) NSUInteger suggestionsCount;
@property(nonatomic) SearchMode searchMode;
@property(nonatomic) BOOL textChanged;
@property(nonatomic) Observers * observers;
@property(nonatomic) NSUInteger lastSearchTimestamp;
@property(nonatomic) SearchIndex * itemsIndex;
@property(nonatomic) NSInteger searchCount;
@end
@implementation MWMSearch {
std::string m_query;
std::string m_locale;
bool m_isCategory;
search::Results m_everywhereResults;
search::Results m_viewportResults;
}
#pragma mark - Instance
+ (MWMSearch *)manager {
static MWMSearch *manager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[self alloc] initManager];
});
return manager;
}
- (instancetype)initManager {
self = [super init];
if (self) {
_observers = [Observers weakObjectsHashTable];
[MWMFrameworkListener addObserver:self];
}
return self;
}
- (void)searchEverywhere {
self.lastSearchTimestamp += 1;
NSUInteger const timestamp = self.lastSearchTimestamp;
search::EverywhereSearchParams params{
m_query, m_locale, {} /* default timeout */, m_isCategory,
// m_onResults
[self, timestamp](search::Results results, std::vector<search::ProductInfo> productInfo)
{
// Store the flag first, because we will make move next.
bool const isEndMarker = results.IsEndMarker();
if (timestamp == self.lastSearchTimestamp)
{
self.suggestionsCount = results.GetSuggestsCount();
self->m_everywhereResults = std::move(results);
[self onSearchResultsUpdated];
}
if (isEndMarker)
self.searchCount -= 1;
}
};
GetFramework().GetSearchAPI().SearchEverywhere(std::move(params));
self.searchCount += 1;
}
- (void)searchInViewport {
search::ViewportSearchParams params {
m_query, m_locale, {} /* default timeout */, m_isCategory,
// m_onStarted
{},
// m_onCompleted
[self](search::Results results)
{
if (!results.IsEndMarker())
return;
if (!results.IsEndedCancelled())
self->m_viewportResults = std::move(results);
}
};
GetFramework().GetSearchAPI().SearchInViewport(std::move(params));
}
- (void)update {
if (m_query.empty())
return;
switch (self.searchMode) {
case SearchModeEverywhere:
[self searchEverywhere];
break;
case SearchModeViewport:
[self searchInViewport];
break;
case SearchModeEverywhereAndViewport:
[self searchEverywhere];
[self searchInViewport];
break;
}
}
#pragma mark - Add/Remove Observers
+ (void)addObserver:(id<MWMSearchObserver>)observer {
[[MWMSearch manager].observers addObject:observer];
}
+ (void)removeObserver:(id<MWMSearchObserver>)observer {
[[MWMSearch manager].observers removeObject:observer];
}
#pragma mark - Methods
+ (void)saveQuery:(SearchQuery *)query {
if (!query.text || query.text.length == 0)
return;
std::string locale = (!query.locale || query.locale == 0)
? [MWMSearch manager]->m_locale
: query.locale.UTF8String;
std::string text = query.text.UTF8String;
GetFramework().GetSearchAPI().SaveSearchQuery({std::move(locale), std::move(text)});
}
+ (void)searchQuery:(SearchQuery *)query {
if (!query.text)
return;
MWMSearch *manager = [MWMSearch manager];
if (query.locale.length != 0)
manager->m_locale = query.locale.UTF8String;
// Pass input query as-is without any normalization (precomposedStringWithCompatibilityMapping).
// Otherwise № -> No, and it's unexpectable for the search index.
manager->m_query = query.text.UTF8String;
manager->m_isCategory = (query.source == SearchTextSourceCategory);
manager.textChanged = YES;
[manager reset];
[manager update];
}
+ (void)showResultAtIndex:(NSUInteger)index {
auto const & result = [MWMSearch manager]->m_everywhereResults[index];
GetFramework().StopLocationFollow();
GetFramework().SelectSearchResult(result, true);
}
+ (SearchResult *)resultWithContainerIndex:(NSUInteger)index {
SearchResult * result = [[SearchResult alloc] initWithResult:[MWMSearch manager]->m_everywhereResults[index]
itemType:[MWMSearch resultTypeWithRow:index]
index:index];
return result;
}
+ (NSArray<SearchResult *> *)getResults {
NSMutableArray<SearchResult *> * results = [[NSMutableArray alloc] initWithCapacity:MWMSearch.resultsCount];
for (NSUInteger i = 0; i < MWMSearch.resultsCount; ++i) {
SearchResult * result = [MWMSearch resultWithContainerIndex:i];
[results addObject:result];
}
return [results copy];
}
+ (SearchItemType)resultTypeWithRow:(NSUInteger)row {
auto itemsIndex = [MWMSearch manager].itemsIndex;
return [itemsIndex resultTypeWithRow:row];
}
+ (NSUInteger)containerIndexWithRow:(NSUInteger)row {
auto itemsIndex = [MWMSearch manager].itemsIndex;
return [itemsIndex resultContainerIndexWithRow:row];
}
- (void)reset {
self.lastSearchTimestamp += 1;
GetFramework().GetSearchAPI().CancelAllSearches();
m_everywhereResults.Clear();
m_viewportResults.Clear();
[self onSearchResultsUpdated];
}
+ (void)clear {
auto manager = [MWMSearch manager];
manager->m_query.clear();
manager.suggestionsCount = 0;
[manager reset];
}
+ (SearchMode)searchMode {
return [MWMSearch manager].searchMode;
}
+ (void)setSearchMode:(SearchMode)mode {
MWMSearch * manager = [MWMSearch manager];
if (manager.searchMode == mode)
return;
manager.searchMode = mode;
[manager update];
}
+ (NSUInteger)suggestionsCount {
return [MWMSearch manager].suggestionsCount;
}
+ (NSUInteger)resultsCount {
return [MWMSearch manager].itemsIndex.count;
}
- (void)updateItemsIndexWithBannerReload:(BOOL)reloadBanner {
auto const resultsCount = self->m_everywhereResults.GetCount();
auto const itemsIndex = [[SearchIndex alloc] initWithSuggestionsCount:self.suggestionsCount
resultsCount:resultsCount];
[itemsIndex build];
self.itemsIndex = itemsIndex;
}
#pragma mark - Notifications
- (void)onSearchStarted {
for (Observer observer in self.observers) {
if ([observer respondsToSelector:@selector(onSearchStarted)])
[observer onSearchStarted];
}
}
- (void)onSearchCompleted {
[self updateItemsIndexWithBannerReload:YES];
for (Observer observer in self.observers) {
if ([observer respondsToSelector:@selector(onSearchCompleted)])
[observer onSearchCompleted];
}
}
- (void)onSearchResultsUpdated {
[self updateItemsIndexWithBannerReload:NO];
for (Observer observer in self.observers) {
if ([observer respondsToSelector:@selector(onSearchResultsUpdated)])
[observer onSearchResultsUpdated];
}
}
#pragma mark - MWMFrameworkDrapeObserver
- (void)processViewportChangedEvent {
if (!GetFramework().GetSearchAPI().IsViewportSearchActive())
return;
BOOL const isSearchCompleted = self.searchCount == 0;
if (!isSearchCompleted)
return;
switch (self.searchMode) {
case SearchModeEverywhere:
case SearchModeViewport:
break;
case SearchModeEverywhereAndViewport:
[self searchEverywhere];
break;
}
}
#pragma mark - Properties
- (void)setSearchCount:(NSInteger)searchCount {
NSAssert((searchCount >= 0) && ((_searchCount == searchCount - 1) || (_searchCount == searchCount + 1)),
@"Invalid search count update");
if (searchCount > 0)
[self onSearchStarted];
else if (searchCount == 0)
[self onSearchCompleted];
_searchCount = searchCount;
}
@end

View file

@ -0,0 +1,8 @@
@protocol MWMSearchObserver<NSObject>
@optional
- (void)onSearchStarted;
- (void)onSearchCompleted;
- (void)onSearchResultsUpdated;
@end

View file

@ -0,0 +1,39 @@
@objc(MWMSearchBanners)
final class SearchBanners: NSObject {
private var banners: [MWMBanner] = []
weak var searchIndex: SearchIndex?
@objc init(searchIndex: SearchIndex) {
self.searchIndex = searchIndex
super.init()
}
@objc func add(_ banner: MWMBanner) {
guard let searchIndex = searchIndex else { return }
banners.append(banner)
let type: MWMSearchItemType
let prefferedPosition: Int
switch banner.mwmType {
case .mopub:
type = .mopub
prefferedPosition = 2
case .facebook:
type = .facebook
prefferedPosition = 2
default:
assert(false, "Unsupported banner type")
type = .regular
prefferedPosition = 0
}
searchIndex.addItem(type: type, prefferedPosition: prefferedPosition, containerIndex: banners.count - 1)
}
@objc func banner(atIndex index: Int) -> MWMBanner {
return banners[index]
}
deinit {
banners.forEach { BannersCache.cache.bannerIsOutOfScreen(coreBanner: $0) }
}
}

View file

@ -0,0 +1,87 @@
@objc
final class SearchIndex: NSObject {
fileprivate struct Item {
let type: SearchItemType
let containerIndex: Int
}
fileprivate struct PositionItem {
let item: Item
var position: Int
}
private var positionItems: [PositionItem] = []
private var items: [Item] = []
@objc var count: Int {
return items.count
}
@objc init(suggestionsCount: Int, resultsCount: Int) {
for index in 0 ..< resultsCount {
let type: SearchItemType = index < suggestionsCount ? .suggestion : .regular
let item = Item(type: type, containerIndex: index)
positionItems.append(PositionItem(item: item, position: index))
}
super.init()
}
func addItem(type: SearchItemType, prefferedPosition: Int, containerIndex: Int) {
assert(type != .suggestion && type != .regular)
let item = Item(type: type, containerIndex: containerIndex)
positionItems.append(PositionItem(item: item, position: prefferedPosition))
}
@objc func build() {
positionItems.sort(by: >)
var itemsDict: [Int: Item] = [:]
positionItems.forEach { item in
var position = item.position
while itemsDict[position] != nil {
position += 1
}
itemsDict[position] = item.item
}
items.removeAll()
let keys = itemsDict.keys.sorted()
for index in 0 ..< keys.count {
let key = keys[index]
if index == key {
items.append(itemsDict[key]!)
}
}
}
@objc func resultType(row: Int) -> SearchItemType {
return items[row].type
}
@objc func resultContainerIndex(row: Int) -> Int {
return items[row].containerIndex
}
}
extension SearchIndex.PositionItem: Equatable {
static func ==(lhs: SearchIndex.PositionItem, rhs: SearchIndex.PositionItem) -> Bool {
let lhsCache = lhs.item
let rhsCache = rhs.item
return lhsCache.type == rhsCache.type &&
lhs.position == rhs.position &&
lhsCache.containerIndex == rhsCache.containerIndex
}
}
extension SearchIndex.PositionItem: Comparable {
static func <(lhs: SearchIndex.PositionItem, rhs: SearchIndex.PositionItem) -> Bool {
let lhsCache = lhs.item
let rhsCache = rhs.item
guard lhsCache.type == rhsCache.type else {
return lhsCache.type.rawValue < rhsCache.type.rawValue
}
guard lhs.position == rhs.position else {
return lhs.position > rhs.position
}
return lhsCache.containerIndex < rhsCache.containerIndex
}
}

View file

@ -0,0 +1,4 @@
typedef NS_ENUM(NSInteger, SearchItemType) {
SearchItemTypeRegular,
SearchItemTypeSuggestion
};

View file

@ -0,0 +1,13 @@
#import "SearchResult.h"
#import "search/result.hpp"
NS_ASSUME_NONNULL_BEGIN
@interface SearchResult (Core)
- (instancetype)initWithResult:(const search::Result &)result itemType:(SearchItemType)itemType index:(NSUInteger)index;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,32 @@
#import "SearchItemType.h"
#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>
NS_ASSUME_NONNULL_BEGIN
@interface SearchResult : NSObject
@property (nonatomic, readonly) NSUInteger index;
@property (nonatomic, readonly) NSString * titleText;
@property (nonatomic, readonly, nullable) NSString * branchText;
@property (nonatomic, readonly) NSString * iconImageName;
@property (nonatomic, readonly) NSString * addressText;
@property (nonatomic, readonly) NSString * infoText;
@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property (nonatomic, readonly) CGPoint point;
@property (nonatomic, readonly, nullable) NSString * distanceText;
@property (nonatomic, readonly, nullable) NSString * openStatusText;
@property (nonatomic, readonly) UIColor * openStatusColor;
@property (nonatomic, readonly) BOOL isPopularHidden;
@property (nonatomic, readonly) NSString * suggestion;
@property (nonatomic, readonly) BOOL isPureSuggest;
@property (nonatomic, readonly) NSArray<NSValue *> * highlightRanges;
@property (nonatomic, readonly) SearchItemType itemType;
/// This initializer is intended only for testing purposes.
- (instancetype)initWithTitleText:(NSString *)titleText type:(SearchItemType)type suggestion:(NSString *)suggestion;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,118 @@
#import "SearchResult+Core.h"
#import "CLLocation+Mercator.h"
#import "MWMLocationManager.h"
#import "SwiftBridge.h"
#import "platform/localization.hpp"
#import "platform/distance.hpp"
#include "map/bookmark_helpers.hpp"
#import "geometry/mercator.hpp"
@implementation SearchResult
- (instancetype)initWithTitleText:(NSString *)titleText type:(SearchItemType)type suggestion:(NSString *)suggestion {
self = [super init];
if (self) {
_titleText = titleText;
_itemType = type;
_suggestion = suggestion;
};
return self;
}
@end
@implementation SearchResult(Core)
- (instancetype)initWithResult:(const search::Result &)result itemType:(SearchItemType)itemType index:(NSUInteger)index {
self = [super init];
if (self) {
_index = index;
_titleText = result.GetString().empty() ? @(result.GetLocalizedFeatureType().c_str()) : @(result.GetString().c_str());
_addressText = @(result.GetAddress().c_str());
_infoText = @(result.GetFeatureDescription().c_str());
_branchText = result.GetBranch().empty() ? nil : @(result.GetBranch().c_str());
if (result.IsSuggest())
_suggestion = @(result.GetSuggestionString().c_str());
_distanceText = nil;
if (result.HasPoint()) {
auto const center = result.GetFeatureCenter();
_point = CGPointMake(center.x, center.y);
auto const [centerLat, centerLon] = mercator::ToLatLon(center);
_coordinate = CLLocationCoordinate2DMake(centerLat, centerLon);
CLLocation * lastLocation = [MWMLocationManager lastLocation];
if (lastLocation) {
double const distanceM = mercator::DistanceOnEarth(lastLocation.mercator, center);
std::string const distanceStr = platform::Distance::CreateFormatted(distanceM).ToString();
_distanceText = @(distanceStr.c_str());
}
}
switch (result.IsOpenNow()) {
case osm::Yes: {
const int minutes = result.GetMinutesUntilClosed();
if (minutes < 60) { // less than 1 hour
_openStatusColor = [UIColor colorNamed:@"Base Colors/Yellow Color"];
NSString * time = [NSString stringWithFormat:@"%d %@", minutes, L(@"minute")];
_openStatusText = [NSString stringWithFormat:L(@"closes_in"), time];
} else {
_openStatusColor = [UIColor colorNamed:@"Base Colors/Green Color"];
_openStatusText = L(@"editor_time_open");
}
break;
}
case osm::No: {
const int minutes = result.GetMinutesUntilOpen();
if (minutes < 15) { // less than 15 minutes
_openStatusColor = [UIColor colorNamed:@"Base Colors/Yellow Color"];
NSString * time = [NSString stringWithFormat:@"%d %@", minutes, L(@"minute")];
_openStatusText = [NSString stringWithFormat:L(@"opens_in"), time];
}
else if (minutes < 60) { // less than an hour (but more than 15 mins)
_openStatusColor = [UIColor colorNamed:@"Base Colors/Red Color"];
NSString * time = [NSString stringWithFormat:@"%d %@", minutes, L(@"minute")];
_openStatusText = [NSString stringWithFormat:L(@"opens_in"), time];
}
else { // opens later or schedule is unknown
_openStatusColor = [UIColor colorNamed:@"Base Colors/Red Color"];
_openStatusText = L(@"closed");
}
break;
}
case osm::Unknown: {
_openStatusText = nil;
_openStatusColor = UIColor.clearColor;
break;
}
}
_isPopularHidden = YES; // Restore logic in the future when popularity is available.
_isPureSuggest = result.GetResultType() == search::Result::Type::PureSuggest;
NSMutableArray<NSValue *> * ranges = [NSMutableArray array];
size_t const rangesCount = result.GetHighlightRangesCount();
for (size_t i = 0; i < rangesCount; ++i) {
auto const &range = result.GetHighlightRange(i);
NSRange nsRange = NSMakeRange(range.first, range.second);
[ranges addObject:[NSValue valueWithRange:nsRange]];
}
_highlightRanges = [ranges copy];
_itemType = itemType;
if (result.GetResultType() == search::Result::Type::Feature) {
auto const featureType = result.GetFeatureType();
auto const bookmarkImage = GetBookmarkIconByFeatureType(featureType);
_iconImageName = [NSString stringWithFormat:@"%@%@",
@"ic_bm_",
[@(kml::ToString(bookmarkImage).c_str()) lowercaseString]];
}
}
return self;
}
@end