diff --git a/apidoc/Map.yml b/apidoc/Map.yml index 9cf981a6..3b980deb 100644 --- a/apidoc/Map.yml +++ b/apidoc/Map.yml @@ -42,13 +42,14 @@ description: | ``` - - Instantiate the module with the `require('ti.map')` method, then make subsequent API calls with + - Instantiate the module with the `import Map from 'ti.map'` API, then make subsequent API calls with the new map object. ``` javascript - var Map = require('ti.map'); - var mapview = Map.createView({ - mapType: Map.NORMAL_TYPE + import Map from 'ti.map'; + + const mapView = Map.createView({ + mapType: Map.NORMAL_TYPE }); ``` @@ -99,12 +100,13 @@ description: | ``` - - Instantiate the module with the `require('ti.map')` method, then make subsequent API calls with + - Instantiate the module with the `import Map from 'ti.map'` API, then make subsequent API calls with the new map object. ``` javascript - var Map = require('ti.map'); - var mapview = Map.createView({ + import Map from 'ti.map'; + + const mapView = Map.createView({ mapType: Map.NORMAL_TYPE }); ``` @@ -482,6 +484,27 @@ properties: osver: {ios: {min: "11.0"} } exclude-platforms: [android] since: "6.3.0" + + - name: SEARCH_RESULT_TYPE_ADDRESS + summary: A value that indicates that search results include addresses. + type: Number + permission: read-only + osver: {ios: {min: "13.0"} } + since: "12.3.0" + + - name: SEARCH_RESULT_TYPE_POINT_OF_INTEREST + summary: A value that indicates that search results include points of interest. + type: Number + permission: read-only + osver: {ios: {min: "13.0"} } + since: "12.3.0" + + - name: SEARCH_RESULT_TYPE_QUERY + summary: A value that indicates that the search completer includes query completions in results. + type: Number + permission: read-only + osver: {ios: {min: "13.0"} } + since: "12.3.0" methods: - name: isGooglePlayServicesAvailable @@ -495,6 +518,43 @@ methods: summary: Returns a code to indicate whether Google Play Services is available on the device. since: "3.1.1" platforms: [android] + + - name: search + summary: | + Uses the native `MKLocalSearchCompleter` class to search places for + a given input value. + description: | + Please use the `didUpdateResults` event to get updates for a search + completion request via the `results` field. If the search failed, the + `results` field is empty and an `error` is provided. + parameters: + - name: value + summary: The value to search with. + type: String + - name: options + summary: Additional options to fine-tune the search request. + type: SearchCompletionOptions + platforms: [iphone, ipad, macos] + since: "12.3.0" + + - name: geocodeAddress + summary: | + Resolve address details using the `CLGeocoder` to get information (e.g. + latitude, longitude, postal code and city) about a given input address. + description: | + The result is provided via the callback (second function argument). + parameters: + - name: address + summary: The address to resolve. + type: String + - name: callback + summary: | + Function to be called upon completion (either success with a place + or an error). + type: Callback + optional: false + platforms: [iphone, ipad, macos] + since: "12.3.0" - name: getLookAroundImage summary: A utility function that you use to create a static image from a LookAround scene. @@ -539,10 +599,11 @@ examples: and it is not practical to identify them by title. ``` javascript - var Map = require('ti.map'); - var win = Titanium.UI.createWindow(); + import Map from 'ti.map'; + + const window = Ti.UI.createWindow(); - var mountainView = Map.createAnnotation({ + const mountainView = Map.createAnnotation({ latitude: 37.390749, longitude: -122.081651, title: 'Appcelerator Headquarters', @@ -551,7 +612,7 @@ examples: myid: 1 // Custom property to uniquely identify this annotation. }); - var mapview = Map.createView({ + const mapView = Map.createView({ mapType: Map.NORMAL_TYPE, region: { latitude: 33.74511, @@ -565,7 +626,7 @@ examples: annotations: [ mountainView ] }); - var circle = Map.createCircle({ + const circle = Map.createCircle({ center: { latitude: 33.74511, longitude: -84.38993 @@ -573,14 +634,15 @@ examples: radius: 1000, // = 1.0 km fillColor: '#20FF0000' }); - mapview.addCircle(circle); - win.add(mapview); + mapView.addCircle(circle); + window.add(mapView); - mapview.addEventListener('click', function(event) { + mapView.addEventListener('click', event => { Ti.API.info('Clicked ' + event.clicksource + ' on ' + event.latitude + ', ' + event.longitude); }); - win.open(); + + windown.open(); ``` - title: Alloy XML Markup example: | @@ -605,7 +667,7 @@ examples: ``` xml - + @@ -615,7 +677,7 @@ examples: `app/styles/index.tss`: ``` javascript - "#mapview": { + "#mapView": { region: { latitude: 33.74511, longitude: -84.38993, @@ -651,11 +713,12 @@ examples: view instance. ``` javascript - var Map = require('ti.map'); - var win = Titanium.UI.createWindow(); - var annotations = []; + import Map from 'ti.map'; + + const window = Ti.UI.createWindow(); + const annotations = []; - for (var i = 0; i < 10; i++) { + for (let i = 0; i < 10; i++) { annotations.push(Map.createAnnotation({ title: 'Appcelerator Inc.', subtitle: 'TiRocks!', @@ -675,7 +738,7 @@ examples: })); } - var mapview = Map.createView({ + const mapView = Map.createView({ annotations: annotations, rotateEnabled: true, mapType: Map.MUTED_STANDARD_TYPE, @@ -683,23 +746,47 @@ examples: userLocation: true }); - mapview.addEventListener('clusterstart', function(e) { + mapView.addEventListener('clusterstart', event => { Ti.API.info('clustering started!'); - var clusterAnnotation = Map.createAnnotation({ + const clusterAnnotation = Map.createAnnotation({ showAsMarker: true, - markerText: e.memberAnnotations.length.toString(), + markerText: event.memberAnnotations.length.toString(), title: 'Cluster Title', subtitle: 'Cluster Subtitle', }); - mapview.setClusterAnnotation({ + mapView.setClusterAnnotation({ annotation: clusterAnnotation, - memberAnnotations: e.memberAnnotations + memberAnnotations: event.memberAnnotations }); }); - win.add(mapview); - win.open(); + window.add(mapView); + window.open(); + ``` + - title: Search Request (iOS only) + example: | + The following example shows the MapKit based search request. + The options in `search` (2nd parameter) are optional, but improve + the accuracy of the results. + + ```javascript + import Map from 'ti.map'; + + Map.addEventListener('didUpdateResults', event => { + console.warn('Found place:'); + console.warn(event) + }); + + Map.search('Colosseum', { + region: { + latitude: 41.890560, + longitude: 12.494270, + latitudeDelta: 1, + longitudeDelta: 1, + }, + resultTypes: [Map.SEARCH_RESULT_TYPE_POINT_OF_INTEREST, Map.SEARCH_RESULT_TYPE_ADDRESS] + }); ``` --- @@ -709,7 +796,25 @@ properties: - name: longitude summary: Longitude value of the map point, in decimal degrees. type: Number - - name: latitude summary: Latitude value of the map point, in decimal degrees. type: Number + +--- +name: SearchCompletionOptions +summary: Additional options to fine-tune the search request. +description: The latitute and longitude describe the location, the delta values the distance to include. +properties: + - name: region + summary: | + The region to look for results. Note that only `latitude`, `longitude`, `latitudeDelta` and `longitudeDelta` + are currently handled to define the region. + type: MapRegionTypev2 + - name: resultTypes + summary: These options configure the types of search results you want to receive, including points of interest and addresses. + description: | + Use one or more of the following constants: + - + - + - + type: Array \ No newline at end of file diff --git a/ios/Classes/TiMapModule.h b/ios/Classes/TiMapModule.h index 5eae48bd..f5bbc629 100644 --- a/ios/Classes/TiMapModule.h +++ b/ios/Classes/TiMapModule.h @@ -9,11 +9,12 @@ #import #if IS_SDK_IOS_16 -@interface TiMapModule : TiModule { +@interface TiMapModule : TiModule { #else @interface TiMapModule : TiModule { #endif UIColor *colorRed; + MKLocalSearchCompleter *_searchCompleter; } @property (nonatomic, readonly) NSNumber *STANDARD_TYPE; diff --git a/ios/Classes/TiMapModule.m b/ios/Classes/TiMapModule.m index dd006ede..d39b3c3b 100644 --- a/ios/Classes/TiMapModule.m +++ b/ios/Classes/TiMapModule.m @@ -10,6 +10,7 @@ #import "TiBlob.h" #import "TiMapCameraProxy.h" #import "TiMapConstants.h" +#import "TiMapUtils.h" #import "TiMapViewProxy.h" @implementation TiMapModule @@ -116,6 +117,138 @@ - (void)lookAroundViewControllerDidPresentFullScreen:(MKLookAroundViewController #endif +- (MKLocalSearchCompleter *)searchCompleter +{ + if (_searchCompleter == nil) { + _searchCompleter = [[MKLocalSearchCompleter alloc] init]; + _searchCompleter.delegate = self; + } + + return _searchCompleter; +} + +- (void)search:(id)args +{ + ENSURE_UI_THREAD(search, args); + + NSString *value = [TiUtils stringValue:args[0]]; + + // Require a search value + if (!value) { + [self throwException:@"Missing required search value" subreason:@"Please provide the value as a String" location:CODELOCATION]; + return; + } + + // Pass additional options like search region + if ([args count] > 1) { + NSDictionary *options = (NSDictionary *)args[1]; + if (options == nil) { + [self throwException:@"Options have to be called within an Object" subreason:@"Please provide the value as an Object" location:CODELOCATION]; + } + + // Handle search region + if (options[@"region"]) { + NSDictionary *region = options[@"region"]; + CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake([TiUtils doubleValue:region[@"latitude"]], [TiUtils doubleValue:region[@"longitude"]]); + MKCoordinateSpan span = MKCoordinateSpanMake([TiUtils doubleValue:region[@"latitudeDelta"]], [TiUtils doubleValue:region[@"longitudeDelta"]]); + + if (CLLocationCoordinate2DIsValid(coordinate)) { + [[self searchCompleter] setRegion:MKCoordinateRegionMake(coordinate, span)]; + } + } + + // Handle filter types + if ([TiUtils isIOSVersionOrGreater:@"13.0"] && options[@"resultTypes"]) { + if (@available(iOS 13.0, *)) { + _searchCompleter.resultTypes = [TiMapUtils mappedResultTypes:options[@"resultTypes"]]; + } else { + NSLog(@"[ERROR] The \"resultTypes\" options are only available on iOS 13+"); + } + } + } + + [[self searchCompleter] setQueryFragment:value]; +} + +- (void)geocodeAddress:(id)args +{ + NSString *address = (NSString *)args[0]; + KrollCallback *callback = (KrollCallback *)args[1]; + + CLGeocoder *geocoder = [[CLGeocoder alloc] init]; + [geocoder geocodeAddressString:address + completionHandler:^(NSArray *_Nullable placemarks, NSError *_Nullable error) { + if (placemarks.count == 0 || error != nil) { + [callback call:@[ @{@"success" : @(NO), + @"error" : error.localizedDescription ?: @"Unknown error"} ] + thisObject:self]; + return; + } + + CLPlacemark *place = placemarks[0]; + + NSDictionary *proxyPlace = @{ + @"name" : NULL_IF_NIL(place.name), + @"street" : NULL_IF_NIL([self formattedStreetNameFromPlace:place]), + @"postalCode" : NULL_IF_NIL(place.postalCode), + @"city" : NULL_IF_NIL(place.locality), + @"country" : NULL_IF_NIL(place.country), + @"state" : NULL_IF_NIL(place.administrativeArea), + @"latitude" : @(place.location.coordinate.latitude), + @"longitude" : @(place.location.coordinate.longitude), + }; + + [callback call:@[ @{@"success" : @(YES), + @"place" : proxyPlace} ] + thisObject:self]; + }]; +} + +- (NSString *)formattedStreetNameFromPlace:(CLPlacemark *)place +{ + if (place.thoroughfare == nil) { + return nil; + } else if (place.subThoroughfare == nil) { + return place.thoroughfare; + } + + return [NSString stringWithFormat:@"%@ %@", place.thoroughfare, place.subThoroughfare]; +} + +- (void)completer:(MKLocalSearchCompleter *)completer didFailWithError:(NSError *)error +{ + [self fireEvent:@"didUpdateResults" withObject:@{ @"results" : @[], @"error" : error.localizedDescription }]; +} + +- (void)completerDidUpdateResults:(MKLocalSearchCompleter *)completer +{ + NSMutableArray *> *proxyResults = [NSMutableArray arrayWithCapacity:completer.results.count]; + + [completer.results enumerateObjectsUsingBlock:^(MKLocalSearchCompletion *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + NSMutableArray *> *titleHighlightRanges = [NSMutableArray arrayWithCapacity:obj.titleHighlightRanges.count]; + NSMutableArray *> *subtitleHighlightRanges = [NSMutableArray arrayWithCapacity:obj.subtitleHighlightRanges.count]; + + [obj.titleHighlightRanges enumerateObjectsUsingBlock:^(NSValue *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + [titleHighlightRanges addObject:@{@"offset" : @(obj.rangeValue.location), + @"length" : @(obj.rangeValue.length)}]; + }]; + + [obj.subtitleHighlightRanges enumerateObjectsUsingBlock:^(NSValue *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + [subtitleHighlightRanges addObject:@{@"offset" : @(obj.rangeValue.location), + @"length" : @(obj.rangeValue.length)}]; + }]; + + [proxyResults addObject:@{ + @"title" : obj.title, + @"subtitle" : obj.subtitle, + @"titleHighlightRanges" : titleHighlightRanges, + @"subtitleHighlightRanges" : subtitleHighlightRanges + }]; + }]; + + [self fireEvent:@"didUpdateResults" withObject:@{ @"results" : proxyResults }]; +} + MAKE_SYSTEM_PROP(STANDARD_TYPE, MKMapTypeStandard); MAKE_SYSTEM_PROP(NORMAL_TYPE, MKMapTypeStandard); // For parity with Android MAKE_SYSTEM_PROP(SATELLITE_TYPE, MKMapTypeSatellite); @@ -164,4 +297,8 @@ - (void)lookAroundViewControllerDidPresentFullScreen:(MKLookAroundViewController MAKE_SYSTEM_PROP(FEATURE_TYPE_POINT_OF_INTEREST, MKMapFeatureOptionPointsOfInterest); #endif +MAKE_SYSTEM_PROP(SEARCH_RESULT_TYPE_ADDRESS, MKLocalSearchCompleterResultTypeAddress); +MAKE_SYSTEM_PROP(SEARCH_RESULT_TYPE_POINT_OF_INTEREST, MKLocalSearchCompleterResultTypePointOfInterest); +MAKE_SYSTEM_PROP(SEARCH_RESULT_TYPE_QUERY, MKLocalSearchCompleterResultTypeQuery); + @end diff --git a/ios/Classes/TiMapUtils.h b/ios/Classes/TiMapUtils.h index 4440cbd3..ece8353c 100644 --- a/ios/Classes/TiMapUtils.h +++ b/ios/Classes/TiMapUtils.h @@ -7,6 +7,7 @@ #import #import +#import @interface TiMapUtils : NSObject @@ -14,4 +15,6 @@ + (NSDictionary *)dictionaryFromPlacemark:(CLPlacemark *)placemark; ++ (MKLocalSearchCompleterResultType)mappedResultTypes:(NSArray *)inputResultTypes; + @end diff --git a/ios/Classes/TiMapUtils.m b/ios/Classes/TiMapUtils.m index 8ae44b37..85aec6fc 100644 --- a/ios/Classes/TiMapUtils.m +++ b/ios/Classes/TiMapUtils.m @@ -56,4 +56,16 @@ + (id)returnValueOnMainThread:(id (^)(void))block return place; } ++ (MKLocalSearchCompleterResultType)mappedResultTypes:(NSArray *)inputResultTypes +{ + MKLocalSearchCompleterResultType resultTypes = 0; + + for (NSNumber *number in inputResultTypes) { + MKLocalSearchCompleterResultType typeValue = [number unsignedIntegerValue]; + resultTypes |= typeValue; + } + + return resultTypes; +} + @end diff --git a/ios/Classes/TiMapViewProxy.h b/ios/Classes/TiMapViewProxy.h index a479d522..0eec9e8d 100644 --- a/ios/Classes/TiMapViewProxy.h +++ b/ios/Classes/TiMapViewProxy.h @@ -65,7 +65,6 @@ - (void)removeImageOverlay:(id)arg; - (void)removeAllImageOverlays:(id)args; - (void)setClusterAnnotation:(id)args; -- (void)setLocation:(id)location; - (NSNumber *)containsCoordinate:(id)args; @end diff --git a/ios/manifest b/ios/manifest index 4bdd1a61..6f0a1dcd 100644 --- a/ios/manifest +++ b/ios/manifest @@ -3,7 +3,7 @@ # during compilation, packaging, distribution, etc. # -version: 7.2.0 +version: 7.3.0 apiversion: 2 architectures: arm64 x86_64 description: External version of Map module diff --git a/ios/map.xcodeproj/project.pbxproj b/ios/map.xcodeproj/project.pbxproj index 2fd7b6d7..a6d15353 100644 --- a/ios/map.xcodeproj/project.pbxproj +++ b/ios/map.xcodeproj/project.pbxproj @@ -183,6 +183,8 @@ DB3656EF1E50BA740040E0B9 /* TiMapConstants.h */, 24DD6CF71134B3F500162E58 /* TiMapModule.h */, 24DD6CF81134B3F500162E58 /* TiMapModule.m */, + CEE33CF119EC4C150005E745 /* TiMapUtils.h */, + CEE33CF219EC4C150005E745 /* TiMapUtils.m */, 24DE9E0F11C5FE74003F90F6 /* TiMapModuleAssets.h */, 24DE9E1011C5FE74003F90F6 /* TiMapModuleAssets.m */, ); @@ -293,8 +295,6 @@ CED651C317D7AA21007C2954 /* TiMapView.m */, CED651C417D7AA21007C2954 /* TiMapViewProxy.h */, CED651C517D7AA21007C2954 /* TiMapViewProxy.m */, - CEE33CF119EC4C150005E745 /* TiMapUtils.h */, - CEE33CF219EC4C150005E745 /* TiMapUtils.m */, 27A609E819F5922B00CA150A /* WildcardGestureRecognizer.h */, 27A609E919F5927000CA150A /* WildcardGestureRecognizer.m */, );