diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index c61bf7bd16..5afcfc69f3 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -580,6 +580,7 @@ 32BFF098299137D60023D067 /* OAUploadGPXFilesTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 32BFF097299137D60023D067 /* OAUploadGPXFilesTask.m */; }; 32C1C4ED2DD4C4F200A053D4 /* OAClickableWayHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 32C1C4E42DD4C4F200A053D4 /* OAClickableWayHelper.mm */; }; 32C1C4EF2DD4C4F200A053D4 /* OAMapSelectionHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 32C1C4E72DD4C4F200A053D4 /* OAMapSelectionHelper.mm */; }; + 32C1EF132FB608B6008BBA40 /* OACollapsablePoiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F01E3C2FB6049D00736C97 /* OACollapsablePoiView.swift */; }; 32C21CB127DFC18E00DE4266 /* OAAlertBottomSheetViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 32C21CB027DFC18E00DE4266 /* OAAlertBottomSheetViewController.mm */; }; 32C21CB327E08EFD00DE4266 /* OAAlertBottomSheetViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32C21CB227E08E4D00DE4266 /* OAAlertBottomSheetViewController.xib */; }; 32C21CB727E0A66B00DE4266 /* OARecordSettingsBottomSheetViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 32C21CB627E0A66A00DE4266 /* OARecordSettingsBottomSheetViewController.m */; }; @@ -604,6 +605,7 @@ 32D7D6D22CD9564D00EB752F /* ObfConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32D7D6D12CD9564D00EB752F /* ObfConstants.swift */; }; 32DA38A82BA9C4D500A3AC3C /* CLLocation+Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = 32DA38A72BA9C4D500A3AC3C /* CLLocation+Extension.m */; }; 32DE2BE42B6D13730025F2B9 /* OATwoButtonsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DE2BE32B6D13730025F2B9 /* OATwoButtonsTableViewCell.swift */; }; + 32E080C22FAB2DCA0095A278 /* SearchByRouteIdTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32E080C12FAB2DC50095A278 /* SearchByRouteIdTask.swift */; }; 32E2A6E027206ACF00F018B5 /* OAQuickSearchCoordinateFormatsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32E2A6DF2720699200F018B5 /* OAQuickSearchCoordinateFormatsViewController.xib */; }; 32E2D30229B9DB5700A17D0B /* ic_custom_coordinates_location@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 32E2D2FE29B9DB5500A17D0B /* ic_custom_coordinates_location@3x.png */; }; 32E2D30329B9DB5700A17D0B /* ic_custom_coordinates_map_center@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 32E2D2FF29B9DB5600A17D0B /* ic_custom_coordinates_map_center@2x.png */; }; @@ -4314,6 +4316,7 @@ 32DA38A62BA9C4C600A3AC3C /* CLLocation+Extension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CLLocation+Extension.h"; sourceTree = ""; }; 32DA38A72BA9C4D500A3AC3C /* CLLocation+Extension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "CLLocation+Extension.m"; sourceTree = ""; }; 32DE2BE32B6D13730025F2B9 /* OATwoButtonsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OATwoButtonsTableViewCell.swift; sourceTree = ""; }; + 32E080C12FAB2DC50095A278 /* SearchByRouteIdTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchByRouteIdTask.swift; sourceTree = ""; }; 32E2A6DF2720699200F018B5 /* OAQuickSearchCoordinateFormatsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OAQuickSearchCoordinateFormatsViewController.xib; sourceTree = ""; }; 32E2D2FE29B9DB5500A17D0B /* ic_custom_coordinates_location@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_custom_coordinates_location@3x.png"; path = "Resources/Icons/ic_custom_coordinates_location@3x.png"; sourceTree = ""; }; 32E2D2FF29B9DB5600A17D0B /* ic_custom_coordinates_map_center@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_custom_coordinates_map_center@2x.png"; path = "Resources/Icons/ic_custom_coordinates_map_center@2x.png"; sourceTree = ""; }; @@ -4332,6 +4335,7 @@ 32ECBEAD2657AD7F005D33BD /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; 32EEB6302E28651B002216B0 /* OATravelSelectionLayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OATravelSelectionLayer.h; sourceTree = ""; }; 32EEB6312E286521002216B0 /* OATravelSelectionLayer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OATravelSelectionLayer.mm; sourceTree = ""; }; + 32F01E3C2FB6049D00736C97 /* OACollapsablePoiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OACollapsablePoiView.swift; sourceTree = ""; }; 32F3A6A92D5E1958008AE4CA /* PoiIconCollectionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoiIconCollectionHandler.swift; sourceTree = ""; }; 32F3A6AB2D5E1A41008AE4CA /* IconsAppearanceCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconsAppearanceCategory.swift; sourceTree = ""; }; 32F6B3902E47A842007F3902 /* OAAmenitySearcher+cpp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OAAmenitySearcher+cpp.h"; sourceTree = ""; }; @@ -12984,6 +12988,7 @@ DA5A7E1C26C563A300F274C7 /* OACollapsableTransportStopRoutesView.h */, DA5A7DF926C563A300F274C7 /* OACollapsableTransportStopRoutesView.mm */, 320DF4112F43799C00F80CE3 /* OACollapsableTravelGuidesView.swift */, + 32F01E3C2FB6049D00736C97 /* OACollapsablePoiView.swift */, DA5A7EB726C563A400F274C7 /* OACollapsableWaypointsView.h */, DA5A7E2426C563A400F274C7 /* OACollapsableWaypointsView.mm */, DA5A7EB426C563A400F274C7 /* OAEditPointViewController.h */, @@ -14092,6 +14097,7 @@ 32EB34472E462D58004F7C75 /* OAAmenitySearcher.h */, 32EB34482E462D68004F7C75 /* OAAmenitySearcher.mm */, 32F6B3902E47A842007F3902 /* OAAmenitySearcher+cpp.h */, + 32E080C12FAB2DC50095A278 /* SearchByRouteIdTask.swift */, DA5A80E026C563A600F274C7 /* OAAppSettings.h */, DA5A80B226C563A600F274C7 /* OAAppSettings.m */, 2C8E8A542D5104E600746A69 /* OAArabicNormalizer.h */, @@ -18302,6 +18308,7 @@ DA8DC3982A0CF47D006C116B /* MapMarkerSideWidget.swift in Sources */, FA6408752D3E8047005FD12D /* WikiImageCard.swift in Sources */, DA8DC39D2A0E2247006C116B /* OAMapViewHelper.mm in Sources */, + 32C1EF132FB608B6008BBA40 /* OACollapsablePoiView.swift in Sources */, DA5A81EB26C563A700F274C7 /* OAHistoryDB.mm in Sources */, 32D253B227F336C200324717 /* OACloudBackupViewController.mm in Sources */, DA5A846526C563A900F274C7 /* OADownloadedRegionsLayer.mm in Sources */, @@ -18840,6 +18847,7 @@ DA5A83EA26C563A800F274C7 /* OACollapsableWaypointsView.mm in Sources */, 27D33B572E7459FA00AD4F70 /* MoveToMyLocationAction.swift in Sources */, DA5A837D26C563A800F274C7 /* GpxUIHelper.swift in Sources */, + 32E080C22FAB2DCA0095A278 /* SearchByRouteIdTask.swift in Sources */, DA21F1BB29BA101E004985BA /* OANetworkRouteDrawable.mm in Sources */, DA4ABF252876DA6800B996EF /* OAFileSettingsItem.mm in Sources */, FA7341142BBC1C2000CBF7EC /* OARequiredMapsResourceViewController.mm in Sources */, diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index 6a67292a12..ef22e72abf 100644 --- a/Resources/Localizations/en.lproj/Localizable.strings +++ b/Resources/Localizations/en.lproj/Localizable.strings @@ -1526,6 +1526,7 @@ "ltr_or_rtl_combine_via_per" = "%@ per %@"; "ltr_or_rtl_combine_with_brackets" = "%@ (%@)"; +"ltr_or_rtl_triple_combine_via_space" = "%@ %@ %@"; "shared_string_beta" = "Beta"; "of" = "%d of %d"; "downloaded_bytes" = "%@ of %@"; @@ -4370,6 +4371,10 @@ "rendering_attr_showClimbingRoutes_description" = "Show climbing routes"; "rendering_attr_showClimbingRoutes_name" = "Climbing routes"; +"route_members" = "Members"; +"route_part_of" = "Part of"; +"multipoligon_related" = "Related"; + "shared_string_view_all" = "View all"; "no_internet_descr" = "Please check your connection and try again."; "no_photos_available" = "No photos available"; diff --git a/Sources/Common/OACollatorStringMatcher.h b/Sources/Common/OACollatorStringMatcher.h index 76fa09472a..82e3543016 100644 --- a/Sources/Common/OACollatorStringMatcher.h +++ b/Sources/Common/OACollatorStringMatcher.h @@ -27,6 +27,7 @@ typedef enum CHECK_CONTAINS, // simple collator equals CHECK_EQUALS, + MULTISEARCH, } StringMatcherMode; diff --git a/Sources/Controllers/Map/Layers/OAPOILayer.h b/Sources/Controllers/Map/Layers/OAPOILayer.h index 3b927965c4..a8e31b7b06 100644 --- a/Sources/Controllers/Map/Layers/OAPOILayer.h +++ b/Sources/Controllers/Map/Layers/OAPOILayer.h @@ -11,4 +11,6 @@ @interface OAPOILayer : OASymbolMapLayer ++ (OATargetPoint *)getTargetPoint:(id)obj; + @end diff --git a/Sources/Controllers/Map/Layers/OAPOILayer.mm b/Sources/Controllers/Map/Layers/OAPOILayer.mm index 3487b9eefd..8a7e97a953 100644 --- a/Sources/Controllers/Map/Layers/OAPOILayer.mm +++ b/Sources/Controllers/Map/Layers/OAPOILayer.mm @@ -1519,6 +1519,11 @@ - (NSString *) getAmenityName:(OAPOI *)amenity #pragma mark - OAContextMenuProvider - (OATargetPoint *)getTargetPoint:(id)obj touchLocation:(CLLocation *)touchLocation +{ + return [self.class getTargetPoint:obj]; +} + ++ (OATargetPoint *)getTargetPoint:(id)obj { if ([obj isKindOfClass:OAPOI.class]) return [self getTargetPoint:obj renderedObject:nil placeDetailsObject:nil]; @@ -1529,7 +1534,7 @@ - (OATargetPoint *)getTargetPoint:(id)obj touchLocation:(CLLocation *)touchLocat return nil; } -- (OATargetPoint *) getTargetPoint:(OAPOI *)poi renderedObject:(OARenderedObject *)renderedObject placeDetailsObject:(BaseDetailsObject *)placeDetailsObject ++ (OATargetPoint *) getTargetPoint:(OAPOI *)poi renderedObject:(OARenderedObject *)renderedObject placeDetailsObject:(BaseDetailsObject *)placeDetailsObject { if (placeDetailsObject) poi = placeDetailsObject.syntheticAmenity; diff --git a/Sources/Controllers/Panels/OAMapPanelViewController.mm b/Sources/Controllers/Panels/OAMapPanelViewController.mm index 65f702bd10..d6191e2b0b 100644 --- a/Sources/Controllers/Panels/OAMapPanelViewController.mm +++ b/Sources/Controllers/Panels/OAMapPanelViewController.mm @@ -1430,7 +1430,7 @@ - (void) showContextMenuWithPoints:(NSArray *)targetPoints sele if (_activeTargetType == OATargetRouteIntermediateSelection && targetPoints.count > 1) { [validPoints addObjectsFromArray:targetPoints]; - if (selectedObjects) + if (!NSArrayIsEmpty(selectedObjects)) [validSelectedObjects addObjectsFromArray:selectedObjects]; } else @@ -1441,7 +1441,7 @@ - (void) showContextMenuWithPoints:(NSArray *)targetPoints sele if ([self processTargetPoint:targetPoint]) { [validPoints addObject:targetPoint]; - if (selectedObjects) + if (!NSArrayIsEmpty(selectedObjects)) [validSelectedObjects addObject:selectedObjects[i]]; } } @@ -1559,16 +1559,26 @@ - (void)showContextMenu:(OATargetPoint *)targetPoint saveState:(BOOL)saveState p - (void)setSelectedObject:(OATargetPoint *)targetPoint { + + OAMapObject *obj = nil; if ([targetPoint.targetObj isKindOfClass:OAMapObject.class]) + { + obj = targetPoint.targetObj; + + } + else if([targetPoint.targetObj isKindOfClass:BaseDetailsObject.class]) + { + BaseDetailsObject *baseDetails = (BaseDetailsObject *) targetPoint.targetObj; + obj = (OAMapObject *) [baseDetails syntheticAmenity]; + } + if (obj != nil) { QVector points; - OAMapObject *obj = targetPoint.targetObj; if (obj.x && obj.x.count > 0) { for (int i = 0; i < obj.x.count; i++) points.push_back(OsmAnd::PointI(obj.x[i].intValue, obj.y[i].intValue)); } - [_mapViewController.mapLayers.contextMenuLayer highlightPolygon:points]; } } diff --git a/Sources/Controllers/TargetMenu/OACollapsablePoiView.swift b/Sources/Controllers/TargetMenu/OACollapsablePoiView.swift new file mode 100644 index 0000000000..ce35731126 --- /dev/null +++ b/Sources/Controllers/TargetMenu/OACollapsablePoiView.swift @@ -0,0 +1,117 @@ +// +// OACollapsablePoiView.swift +// OsmAnd +// +// Created by Max Kojin on 14/05/26. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +@objcMembers +final class OACollapsablePoiView: OACollapsableView { + + private let kButtonHeight: CGFloat = 36.0 + + private var titles = [String]() + private var amenities = [OAPOI]() + private var buttons = [OAButton]() + private var selectedButtonIndex = 0 + + func setData(titles: [String], amenities: [OAPOI]) { + self.amenities = amenities + self.titles = titles + buildViews() + } + + func updateLayout(width: CGFloat) { + var y: CGFloat = 0.0 + var viewHeight: CGFloat = 0.0 + var i = 0 + for button in buttons { + if i > 0 { + y += kButtonHeight + 10.0 + viewHeight += 10.0 + } + + let height: CGFloat = kButtonHeight + button.frame = CGRect(x: kMarginLeft, y: y, width: width - kMarginLeft - kMarginRight, height: height) + viewHeight += button.frame.size.height + i += 1 + } + + viewHeight += 8.0 + frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: width, height: viewHeight) + } + + private func buildViews() { + for i in 0.. OAButton { + let btn = OAButton(type: .system) + btn.setTitle(title, for: .normal) + btn.contentHorizontalAlignment = .left + btn.contentEdgeInsets = UIEdgeInsets(top: 0, left: 12.0, bottom: 0, right: 12.0) + btn.titleLabel?.lineBreakMode = .byTruncatingTail + btn.titleLabel?.font = .preferredFont(forTextStyle: .footnote) + btn.layer.cornerRadius = 4.0 + btn.layer.masksToBounds = true + btn.layer.borderWidth = 0.8 + btn.layer.borderColor = UIColor.customSeparator.cgColor + btn.setBackgroundImage(OAUtilities.image(with: .clear), for: .normal) + btn.tintColor = UIColor.iconColorActive + btn.delegate = self + return btn + } + + override func copy(_ sender: Any?) { + guard buttons.count > selectedButtonIndex else { return } + let button = buttons[selectedButtonIndex] + let pasteboard = UIPasteboard.general + pasteboard.string = button.titleLabel?.text + } + + private func updateButtonBorderColor() { + for button in buttons { + button.layer.borderColor = UIColor.customSeparator.cgColor + } + } + + override var canBecomeFirstResponder: Bool { + true + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + updateButtonBorderColor() + } + } + + override func adjustHeight(forWidth width: CGFloat) { + updateLayout(width: width) + } +} + +extension OACollapsablePoiView: OAButtonDelegate { + + func onButtonTapped(_ tag: Int) { + guard amenities.count > tag else { return } + let amenity = amenities[tag] + if let targetPoint = OAPOILayer.getTargetPoint(amenity) { + targetPoint.centerMap = true + OARootViewController.instance().mapPanel.showContextMenu(with: [targetPoint], selectedObjects: [], touchPointLatLon: CLLocation(latitude: targetPoint.location.latitude, longitude: targetPoint.location.longitude)) + } + } + + func onButtonLongPressed(_ tag: Int) { + selectedButtonIndex = tag + guard buttons.count > selectedButtonIndex else { return } + OAUtilities.showMenu(in: self, from: buttons[selectedButtonIndex]) + } +} diff --git a/Sources/Controllers/TargetMenu/OATargetInfoViewController.mm b/Sources/Controllers/TargetMenu/OATargetInfoViewController.mm index 4af1ac0d9e..3d1882951a 100644 --- a/Sources/Controllers/TargetMenu/OATargetInfoViewController.mm +++ b/Sources/Controllers/TargetMenu/OATargetInfoViewController.mm @@ -80,6 +80,9 @@ NSString * const TYPE_WIKIDATA_PHOTO = @"wikidata-photo"; static NSString *WITHIN_POLYGONS_ROW_KEY = @"within_polygons"; +static NSString * const ROUTE_MEMBERS_ROW_KEY = @"route_members_row_key"; +static NSString * const ROUTE_PART_OF_ROW_KEY = @"route_part_of_row_key"; +static NSString * const ROUTE_RELATED_ROUTES_ROW_KEY = @"route_related_routes_row_key"; // HTML for ViewPort static NSString *const kViewPortHtml = @"
"; @@ -212,6 +215,16 @@ + (UIImage *) getIcon:(NSString *)fileName size:(CGSize)size return img; } +- (OAPOI *) getTargetPoiIfExisted +{ + id obj = [self getTargetObj]; + if (obj && [[self getTargetObj] isKindOfClass:OAPOI.class]) + { + return ((OAPOI *) obj); + } + return nil; +} + - (void) buildTopInternal:(NSMutableArray *)rows { [self buildDescription:rows]; @@ -485,10 +498,9 @@ - (void)buildNearestWikiRow:(NSMutableArray *)rows listener: } else { - id targetObj = [self getTargetObj]; - if ([targetObj isKindOfClass:OAPOI.class]) + OAPOI *poi = [self getTargetPoiIfExisted]; + if (poi) { - OAPOI *poi = (OAPOI *) targetObj; [self processNearestWiki:poi]; NSArray *nearest = _nearestWiki; @@ -532,8 +544,8 @@ - (void)buildNearestPoiRow:(NSMutableArray *)rows listener:( if (_isFetchingNearestPoi) return; - OAPOI *poi = [self getTargetObj]; - if (![poi isKindOfClass:OAPOI.class] || ![self showNearestPoi]) + OAPOI *poi = [self getTargetPoiIfExisted]; + if (!poi || ![self showNearestPoi]) return; OAPOIUIFilter *filter = [self getPoiFilterForType:poi isWiki:NO]; @@ -618,33 +630,78 @@ static inline BOOL OARowsContainKey(NSArray *rows, NSString - (void)buildRouteRows:(NSMutableArray *)rows { - // TODO: implement + OAPOI *amenity = [self getTargetPoiIfExisted]; + if (!amenity) + return; -// if (amenity == null) { -// return; -// } -// WeakReference viewGroupRef = new WeakReference<>(viewGroup); -// int position = viewGroup.getChildCount(); -// if (amenity.getAdditionalInfo(Amenity.ROUTE_MEMBERS_IDS) != null) { -// -// buildRouteRow(amenities -> { -// String title = app.getString(R.string.route_members); -// buildRouteRow(amenities, viewGroupRef, position, ROUTE_MEMBERS_ROW_KEY, title); -// }, SearchType.MEMBERS); -// } -// -// if (amenity.getAdditionalInfo(Amenity.ROUTE_ID) != null) { -// -// buildRouteRow(amenities -> { -// String title = app.getString(R.string.route_part_of); -// buildRouteRow(amenities, viewGroupRef, position, ROUTE_PART_OF_ROW_KEY, title); -// }, SearchType.PART_OF); -// -// buildRouteRow(amenities -> { -// String title = app.getString(R.string.multipoligon_related); -// buildRouteRow(amenities, viewGroupRef, position, ROUTE_RELATED_ROUTES_ROW_KEY, title); -// }, SearchType.RELATED); -// } + if (!NSStringIsEmpty([amenity getAdditionalInfo:ROUTE_MEMBERS_IDS])) + { + [self buildRouteRow:rows tag:ROUTE_MEMBERS_ROW_KEY searchType:EOASearchByRouteIdTaskSearchTypeMembers completionHandler:^(NSArray * _Nullable amenities) { + if (!NSArrayIsEmpty(amenities)) + { + NSString *title = OALocalizedString(@"route_members"); + OAAmenityInfoRow *row = [self buildRouteRow:rows amenities:amenities key:ROUTE_MEMBERS_ROW_KEY title:title]; + [self appendInfoRow:row]; + } + }]; + } + + if (!NSStringIsEmpty([amenity getAdditionalInfo:ROUTE_ID])) + { + [self buildRouteRow:rows tag:ROUTE_PART_OF_ROW_KEY searchType:EOASearchByRouteIdTaskSearchTypePartOf completionHandler:^(NSArray * _Nullable amenities) { + if (!NSArrayIsEmpty(amenities)) + { + NSString *title = OALocalizedString(@"route_part_of"); + OAAmenityInfoRow *row = [self buildRouteRow:rows amenities:amenities key:ROUTE_PART_OF_ROW_KEY title:title]; + [self appendInfoRow:row]; + } + }]; + + [self buildRouteRow:rows tag:ROUTE_RELATED_ROUTES_ROW_KEY searchType:EOASearchByRouteIdTaskSearchTypeRelated completionHandler:^(NSArray * _Nullable amenities) { + if (!NSArrayIsEmpty(amenities)) + { + NSString *title = OALocalizedString(@"multipoligon_related"); + OAAmenityInfoRow *row = [self buildRouteRow:rows amenities:amenities key:ROUTE_RELATED_ROUTES_ROW_KEY title:title]; + [self appendInfoRow:row]; + } + }]; + } +} + +- (void)buildRouteRow:(NSMutableArray *)rows tag:(NSString *)tag searchType:(EOASearchByRouteIdTaskSearchType)searchType completionHandler:(void (^ _Nullable)(NSArray * _Nullable amenities))completionHandler +{ + OAPOI *amenity = [self getTargetPoiIfExisted]; + if (amenity) + { + SearchByRouteIdTask *task = [[SearchByRouteIdTask alloc] initWithAmenity:amenity searchType:searchType completionHandler:completionHandler]; + [task execute]; + } +} + +- (OAAmenityInfoRow *)buildRouteRow:(NSMutableArray *)rows amenities:(NSArray *)amenities key:(NSString *)key title:(NSString *)title +{ + NSString *type = [NSString stringWithFormat:@"\"%@\"", [self getTypeStr]]; + NSString *count = [NSString stringWithFormat:@"(%lu)", amenities.count]; + NSString *text = [NSString stringWithFormat:OALocalizedString(@"ltr_or_rtl_triple_combine_via_space"), title, type, count]; + + UIImage *icon = [self getIcon]; + if (!icon && [self getTargetObj]) + icon = [[OAPOILayer getTargetPoint:[self getTargetObj]] icon]; + + OAAmenityInfoRow *row = [[OAAmenityInfoRow alloc] initWithKey:key icon:icon textPrefix:nil text:text textColor:nil isText:YES needLinks:NO order:0 typeName:nil isPhoneNumber:NO isUrl:NO]; + + NSMutableArray *titles = [NSMutableArray new]; + for (OAPOI *amenity in amenities) + { + NSString * title = [[OAPOILayer getTargetPoint:amenity] title]; + [titles addObject:title ? title : @""]; + } + + OACollapsablePoiView *collapsableView = [[OACollapsablePoiView alloc] init]; + [collapsableView setDataWithTitles:titles amenities:amenities]; + row.collapsableView = collapsableView; + + return row; } - (void)buildPluginRows:(NSMutableArray *)rows @@ -918,9 +975,9 @@ - (void)sendNearbyOtherImagesRequest:(NSMutableArray *)cards return; NSString *openPlaceReviewsTagContent = nil; - if ([self.getTargetObj isKindOfClass:OAPOI.class]) + OAPOI *poi = [self getTargetPoiIfExisted]; + if (poi) { - OAPOI *poi = self.getTargetObj; openPlaceReviewsTagContent = @(poi.obfId >> 1).stringValue; } @@ -1334,8 +1391,8 @@ - (void)didTapWiki:(OAAmenityInfoRow *)info if (![self isKindOfClass:OAPOIViewController.class]) return; - id target = [self getTargetObj]; - if (![target isKindOfClass:OAPOI.class]) + OAPOI *poi = [self getTargetPoiIfExisted]; + if (!poi) return; if (!isWikiPurchased) @@ -1345,7 +1402,7 @@ - (void)didTapWiki:(OAAmenityInfoRow *)info } OAWikiWebViewController *wikiController = - [[OAWikiWebViewController alloc] initWithPoi:target]; + [[OAWikiWebViewController alloc] initWithPoi:poi]; [OARootViewController.instance.mapPanel.navigationController pushViewController:wikiController @@ -1803,9 +1860,9 @@ - (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath else if ([info.typeName isEqualToString:kShortDescriptionTravelRowType]) { NSString *routeId = info.hiddenUrl; - if (!NSStringIsEmpty(routeId) && [[self getTargetObj] isKindOfClass:OAPOI.class]) + OAPOI *poi = [self getTargetPoiIfExisted]; + if (!NSStringIsEmpty(routeId) && poi) { - OAPOI *poi = [self getTargetObj]; NSDictionary *routeIdMap = @{routeId : [poi getLocation]}; SearchTravelArticlesTask *task = [[SearchTravelArticlesTask alloc] initWithRouteIds:routeIdMap callback:^(NSDictionary *> * _Nonnull result) { @@ -1878,9 +1935,9 @@ - (void)startLoadingImages } onlinePhotoCardsView.isLoading = YES; - if ([self.getTargetObj isKindOfClass:OAPOI.class]) + OAPOI *poi = [self getTargetPoiIfExisted]; + if (poi) { - OAPOI *poi = self.getTargetObj; onlinePhotoCardsView.title = poi.nameLocalized ?: poi.name; } diff --git a/Sources/Helpers/OAAmenitySearcher.h b/Sources/Helpers/OAAmenitySearcher.h index 6829ec6dd0..8a64ad293b 100644 --- a/Sources/Helpers/OAAmenitySearcher.h +++ b/Sources/Helpers/OAAmenitySearcher.h @@ -65,6 +65,9 @@ NS_ASSUME_NONNULL_BEGIN + (NSArray *)filterUniqueAmenitiesByOsmIdOrWikidata:(NSArray *)amenities; +- (NSArray *)searchRoutePartOf:(NSString *)routeId; +- (NSDictionary *> *)searchRouteMembers:(NSString *)multipleSearch; + @end diff --git a/Sources/Helpers/OAAmenitySearcher.mm b/Sources/Helpers/OAAmenitySearcher.mm index aabbbed532..aa24b85dc9 100644 --- a/Sources/Helpers/OAAmenitySearcher.mm +++ b/Sources/Helpers/OAAmenitySearcher.mm @@ -579,7 +579,7 @@ - (nullable OAPOI *)findByName:(NSArray *)amenities names:(NSArray return [NSArray arrayWithArray:arr]; } +- (NSArray *)searchRoutePartOf:(NSString *)routeId +{ + NSMutableArray *result = [NSMutableArray new]; + QString qRouteMebersIdKey = QString::fromNSString(ROUTE_MEMBERS_IDS); + QString qRouteId = QString::fromNSString(routeId); + + OAResultMatcher *matcher = [[OAResultMatcher alloc] initWithPublishFunc:^BOOL(__autoreleasing id *objectPtr) { + if (objectPtr == nil || *objectPtr == nil) + return false; + + NSValue *value = (NSValue *)*objectPtr; + const OsmAnd::ISearch::IResultEntry *resultEntry = static_cast([value pointerValue]); + + if (resultEntry) + { + const auto amenity = OAGetAmenityFromSearchResult(*resultEntry); + if (amenity) + { + QHash valuesHash = amenity->getDecodedValuesHash(); + const auto it = valuesHash.constFind(qRouteMebersIdKey); + if (it != valuesHash.constEnd()) + { + const QString members = it.value(); + if (!members.isEmpty()) + { + const QStringList ids = members.split(QLatin1Char(' '), Qt::SkipEmptyParts); + if (ids.contains(qRouteId)) + { + OAPOI *poi = [OAAmenitySearcher parsePOI:*resultEntry]; + if (poi) + { + [result addObject:poi]; + return YES; + } + } + } + } + } + } + return NO; + + } cancelledFunc:^BOOL{ + return false; + }]; + + [self searchRouteByName:routeId mode:OsmAnd::StringMatcherMode::CHECK_EQUALS_FROM_SPACE matcher:matcher]; + return result; +} + +- (NSDictionary *> *)searchRouteMembers:(NSString *)multipleSearch +{ + NSMutableArray *result = [NSMutableArray new]; + QString qRouteIdKey = QString::fromNSString(ROUTE_ID); + + QSet routeIds; + NSArray *components = [multipleSearch componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + for (NSString *routeId in components) + { + if (routeId.length > 0) + routeIds.insert(QString::fromNSString(routeId)); + } + + + OAResultMatcher *matcher = [[OAResultMatcher alloc] initWithPublishFunc:^BOOL(__autoreleasing id *objectPtr) { + if (objectPtr == nil || *objectPtr == nil) + return false; + + NSValue *value = (NSValue *)*objectPtr; + const OsmAnd::ISearch::IResultEntry *resultEntry = static_cast([value pointerValue]); + if (resultEntry) + { + const auto amenity = OAGetAmenityFromSearchResult(*resultEntry); + if (amenity) + { + QHash valuesHash = amenity->getDecodedValuesHash(); + const auto it = valuesHash.constFind(qRouteIdKey); + if (it != valuesHash.constEnd()) + { + const QString qRouteIdValue = it.value(); + if (routeIds.contains(qRouteIdValue)) + { + OAPOI *poi = [OAAmenitySearcher parsePOI:*resultEntry]; + if (poi) + { + [result addObject:poi]; + return YES; + } + } + } + } + } + return NO; + + } cancelledFunc:^BOOL{ + return false; + }]; + + [self searchRouteByName:multipleSearch mode:OsmAnd::StringMatcherMode::MULTISEARCH matcher:matcher]; + NSMutableDictionary *> *map = [NSMutableDictionary new]; + + for (OAPOI *am in result) + { + NSString *routeId = [am getAdditionalInfo:ROUTE_ID]; + if (map[routeId] == nil) + { + map[routeId] = [NSMutableArray new]; + } + [map[routeId] addObject:am]; + } + + return map; +} + +- (void)searchRouteByName:(NSString *)multipleSearch mode:(OsmAnd::StringMatcherMode)mode matcher:(OAResultMatcher *)matcher +{ + const auto& obfsCollection = _app.resourcesManager->obfsCollection; + std::shared_ptr ctrl; + ctrl.reset(new OsmAnd::FunctorQueryController([&matcher] + (const OsmAnd::FunctorQueryController* const controller) + { + return matcher && [matcher isCancelled]; + })); + const std::shared_ptr& searchCriteria = std::shared_ptr(new OsmAnd::AmenitiesByNameSearch::Criteria); + + searchCriteria->name = QString::fromNSString(multipleSearch); + searchCriteria->obfInfoAreaFilter = OsmAnd::AreaI(0, 0, INT_MAX, INT_MAX); + searchCriteria->matcherMode = mode; + + const auto search = std::shared_ptr(new OsmAnd::AmenitiesByNameSearch(obfsCollection)); + search->performSearch(*searchCriteria, + [&matcher] + (const OsmAnd::ISearch::Criteria& criteria, const OsmAnd::ISearch::IResultEntry& resultEntry) + { + [matcher publish:[NSValue valueWithPointer:&resultEntry]]; + }, + ctrl); +} + @end diff --git a/Sources/Helpers/SearchByRouteIdTask.swift b/Sources/Helpers/SearchByRouteIdTask.swift new file mode 100644 index 0000000000..673e2c9924 --- /dev/null +++ b/Sources/Helpers/SearchByRouteIdTask.swift @@ -0,0 +1,108 @@ +// +// SearchByRouteIdTask.swift +// OsmAnd +// +// Created by Max Kojin on 06/05/26. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +typealias SearchByRouteIdTaskResultBlock = (_ result: Any?) -> Void + +@objcMembers +final class SearchByRouteIdTask: OAAsyncTask { + + @objc(EOASearchByRouteIdTaskSearchType) + enum SearchType: Int { + case related + case partOf + case members + } + + var completionHandler: (([OAPOI]) -> Void)? + + private var amenity: OAPOI? + private var searchType: SearchType + + private var routeId: String? + private var routeMembersIds: String? + + init(amenity: OAPOI?, searchType: SearchType, completionHandler: (([OAPOI]) -> Void)?) { + self.completionHandler = completionHandler + if let amenity { + self.routeId = amenity.getAdditionalInfo(ROUTE_ID) + self.routeMembersIds = amenity.getAdditionalInfo(ROUTE_MEMBERS_IDS) + } + + self.searchType = searchType + self.amenity = amenity + super.init() + } + + override func doInBackground() -> Any? { + var amenities = [OAPOI]() + let amenitySearcher = OAAmenitySearcher() + + if searchType == .members { + if let routeMembersIds, !routeMembersIds.isEmpty { + let members = amenitySearcher.searchRouteMembers(routeMembersIds) + + for entry in members { + let amenityList = entry.value + if !amenityList.isEmpty { + amenities.append(amenityList[0]) + } + } + } + } else if searchType == .related { + if let routeId, !routeId.isEmpty { + let related = amenitySearcher.searchRouteMembers(routeId) + var amenityList = [OAPOI]() + + for entry in related { + let relatedList = entry.value + if !relatedList.isEmpty { + amenityList.append(contentsOf: relatedList) + } + } + + var seenCoords = Set() + for am in amenityList { + let loc = am.getLocation() + let lat = loc.coordinate.latitude + let lon = loc.coordinate.longitude + let key = String(format: "%.5f,%.5f", lat, lon) + if !seenCoords.contains(key) { + if let amenity, amenity.obfId != am.obfId { + amenities.append(am) + } else if amenity == nil { + amenities.append(am) + } + } + seenCoords.insert(key) + } + } + } else if searchType == .partOf { + if let routeId, !routeId.isEmpty { + let list = amenitySearcher.searchRoutePart(of: routeId) + var routeIdHash = Set() + + for am in list { + if let routeId = am.getAdditionalInfo(ROUTE_ID) { + if !routeIdHash.contains(routeId) { + amenities.append(am) + } + routeIdHash.insert(routeId) + } + } + } + } + return amenities + } + + override func onPostExecute(result: Any?) { + if let completionHandler, let amenities = result as? [OAPOI] { + completionHandler(amenities) + } + } +} +