04 Searching
Overview
Searching Functions
Base
You can use your results from Tutorial03
as a base for our new project.
Search basics
!!! important: Every search besides the Point Of Interest (POI) search will only take place in the currently opened map. To search in another map, you have to switch to the one you would like to search by calling: "[NSDK_Navigation changeMap: error:]")
The Navigation-SDK provides multiple possibilities for searching towns, streets in towns, house numbers in streets, point of interests and much more:
Search method | Description | Depends on |
---|---|---|
[NSDK_Navigation searchTown:count:error:] | Search a town by a given name or name fragment | - |
[NSDK_Navigation searchStreet:count:townSearchResult:error:] | Search a street in a former town or postcode result by a name or name fragment of the street | town result |
[NSDK_Navigation searchHouseNr:count:townSearchResult:streetSearchResult:error:] | Search a house number in a former found town and street result | town, street result |
[NSDK_Navigation searchCrossing:count:streetSearchResult:error:] | Search crossings in a former found street result | street result |
[NSDK_Navigation searchPOI:layerId:count:error:] | Search POIs with a given name around a position or globally | - |
[NSDK_Navigation searchPostcode:count:mergeTowns: majorOnly:distinctHouseNumber:isAmbiguous:error:] | Search a town by postcode | - |
[NSDK_Navigation simpleGeoCode] not yet implemented | Search a complete given address and returns it's position | - |
[NSDK_Navigation simpleInvGeoCode] not yet implemented | Search an address of a given position | - |
As you can see, some of the searches like the street search need a former result. You can only search a street in a previous found town and you can only search house numbers in a known street of a known town.
Adding a simple town search
Every search is built up by the following steps:
- Fill a search request with what you want to search
- Call the appropriate search function with the request as parameter
- Get a single generic result by calling NSDK_SearchResult * result = [NSDK_SearchResults objectAtIndexedSubscript: ] with the wanted index
First, let us add a button to our ViewController Scene in the Main.storyboard. For now, we use it to start the search. Later on, we will use it to switch to a search ViewController. This is explained in one of the next chapters. Ctrl-Drag the button to ViewController.h and create a method for the action "Touch up Inside".
- (IBAction)buttonSearchTouchUp:(id)sender;
We realize a simple town search example by adding the following code to ViewController.m:
- (IBAction)buttonSearchTouchUp:(id)sender { NSDK_SearchRequest* request = [[NSDK_SearchRequest alloc] initWithSearchString:@"karls" kind:NSDK_SK_TOWNBYNAME_SORTED]; NSError * error; NSDK_SearchResults * results = [NSDK_Navigation searchTown:request count:100 mergeTowns:YES error:&error]; for (NSDK_SearchResult * result in results) { float latitude = (float)[[[result getPosition] toGeoPosition] getLatitude]; latitude = latitude / NSDK_GEOCONVERSION_FACTOR; float longitude = (float)[[[result getPosition] toGeoPosition] getLongitude]; longitude = longitude / NSDK_GEOCONVERSION_FACTOR; NSLog(@"Results:%@ Latitude:%f Longitude:%f", [result getName], latitude, longitude); } }
Start the program and take a look into the log output. We get a bunch of cities which all have a name beginning with "karls".
Now comes a step by step introduction of the previous code to get a more detailed view:
First, we fill out the NSDK_SearchRequest request with the needed parameters:
NSDK_SearchRequest* request = [[NSDK_SearchRequest alloc] initWithSearchString:@"karls" kind:NSDK_SK_TOWNBYNAME_SORTED];
The NSDK_SearchRequest class takes the following parameters in its init methods:
- SearchString: The name (or name fragment of the town we would like to search, "karls" in our example)
- Position: A position around the search should take place. If set to "nil" we search alphabetically, not in a radius around a position
- CC (Country Code): An optional country code to filter the result (useful for maps that consists of multiple countries). If set to 0, we will get every town that matches
- SearchKind: The type of search (we want a town search result which orders the towns in a reasonable way (biggest towns first, then districts of towns)
so we set it to NSDK_SK_TOWNBYNAME_SORTED. For all possible values, see enum
NSDK_SearchKind
)
The search itself is called by:
NSDK_SearchResults * results = [NSDK_Navigation searchTown:request count:100 mergeTowns:YES error:&error];
- The first parameter is the above filled request
- The second parameter declares the maxium number of results that should be returned
- The third parameter means "one entry per town" When set, towns which have the same name but different postcodes will be filtered out.
The return value of the search is a iterable of NSDK_SearchResult objects.
The NSDK_SearchResult object holds all data of a single result:
- Kind: The kind of the result, see NSDK_SearchKind for details
- Name: The name of the result item
- CC (Country Code): The country code of the item
- Cat (Category): The category of the result, not ued for house numbers
- Type: The returned kind of the result item (same type as Kind)
- Position: the position of the result item
The log output is generated by:
float latitude = (float)[[[result getPosition] toGeoPosition] getLatitude]; latitude = latitude / NSDK_GEOCONVERSION_FACTOR; float longitude = (float)[[[result getPosition] toGeoPosition] getLongitude]; longitude = longitude / NSDK_GEOCONVERSION_FACTOR; NSLog(@"Results:%@ Latitude:%f Longitude:%f", [result getName], latitude, longitude);
Because the geodecimal position is hold internally as an integer value, we have to cast the result latitude and longitude values to float and divide it with the constant value NSDK_GEOCONVERSION_FACTOR.
Creating an interactive search
The next thing we do is to implement an interactive address search. For this, we have to add and change some code in our project:
Go to Menu "File->New" and choose to create a new "Cocoa Touch Class" with name "SearchViewController". For the subclass choose "UIViewController".
Open Main.storyboard
and remove the "Touch up Inside" action of the Button.
Now add a second ViewController Scene and Ctrl-Drag from the button to the freshly created scene. Choose to create a segue of type "Show". Connect the "SearchViewController" class to the new UIViewController by setting it in the right pane in XCode in the field "Custom Class". Add four Buttons labeled with "Country", "Town", "Street" and "House number" on the top and a Map View below them.
The current state in Interface Builder should look similar to this screenshot:
For displaying the result list of a search, we have to create one more UIViewController, the ResultViewController
.
ResultViewController
This UIViewController will have a text input field at the top and will show search results in a UITableView.
Create a new Cocoa Touch class ResultViewController
(again subclassed from UIViewController) and wire it with a new "View Controller Scene" in Interface Builder (by setting "Custom Class").
Add a UITextField on top and a UITableView below. Furthermore add a "Back" Button
Ctrl-Drag from the three Buttons "Town", "Street" and "House number" of the "SearchViewController" to the "ResultViewController" Scene and add three Segues of type "Show". Set the following identifier of the segues: "segueSearchViewToResultViewTown", "segueSearchViewToResultViewStreet" and "segueSearchViewToResultViewHNR".
Before we can attach the Back Button to a segue, we have to define a method where to jump to. So open the SearchViewController.m and define the following method
- (IBAction)unwindToSearchViewController:(UIStoryboardSegue*)sender { }
XCode recognizes this method as a valid "Exit" Method. So, again open Main.storyboard and Ctrl-Drag from the "Back" Button to the Exit icon (this is the top right icon of any View Controller Scene) and choose the just created "unwindToSearchViewController:" method. Set the identifier of this segue to "segueResultViewToSearchView".
The resulting storyboard should look similar to this:
The "Country" selection is implemented with an "UIPickerView" within the SearchViewController. In the Picker we want to display the countries which are included in the installed map(s).
The following code retrieves the installed maps and stores the available countries into the member "_countries", which is an Array of Country objects (The Country class is a small convenience class which is included in the XCode sample projects):
NSDK_MapInformationResults * mapInformationResults = [NSDK_Navigation getAvailableMaps]; for (int i=0; i < mapInformationResults.size; i++) { NSDK_MapInformation * mi = mapInformationResults[i]; // Add supported countries into our _countries member NSArray <NSString*> * countries = [mi getCountries]; NSError * error; for (NSString * country in countries) { int ptvCode = [NSDK_Navigation transformCountryCodeToPtvCode:PtvAlpha countryCode:country error:&error]; BOOL isMajorRoad = [mi getDetailLevel] == 1; Country * country = [[Country alloc] initWithPtvCode:ptvCode mapId:i majorRoad:isMajorRoad ]; [_countries addObject:country]; } }
!!! important: The DetailLevel "1" means, that it's not a full featured map, but only the "major roads" (the highways) are available.
For the details of how to implement the "UIPickerView" please have a look at the source code of the Tutorial XCode project. In this project you also find the file "Localizable.strings" which contains the displayable name of the countries. Please copy this file to your project.
What's important is, that after the user has selected another country, the SDK has to be informed that the following searches have to be done on a different map:
[NSDK_Navigation changeMap:country.mapId error:&error];
Please start the app and verify that you can navigate through all ViewControllers.
In the next steps, we fill the UIViewControllers with logic and also create a SearchModel which performs the actual search.
SearchModel
Create a new Cocoa Touch class SearchModel
.
The SearchModel provides methods to search towns, streets and house numbers, to get the count of the results and the results itself.
The search methods look very similar to the first search we created in the ViewController. As you can see, the street search needs a former town search result,
and the house number search needs a town and a street search result.
For convenience reasons, the SearchModel object will be designed as a singleton.
The SearchModel class should look like this:
#import "SearchModel.h" @interface SearchModel() @property NSDK_SearchKind searchKind; @property (nonatomic, retain) NSDK_SearchResults * mTownResults; @property (nonatomic, retain) NSDK_SearchResults * mStreetResults; @property (nonatomic, retain) NSDK_SearchResults * mHNRResults; @end static const int MAX_TOWN_HITS = 500; static const int MAX_STREET_HITS = 500; static const int MAX_HNR_HITS = 20; @implementation SearchModel + (instancetype) sharedInstance { static SearchModel * sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[SearchModel alloc] privateInit]; }); return sharedInstance; } - (id) privateInit { id me = [super init]; if (me) { _searchKind = NSDK_SK_TOWNBYNAME_SORTED; } return me; } - (void) searchTown:(NSString*) name countryCode:(int) cc { [self resetTownSearch]; NSError * error; NSDK_SearchRequest* request = [[NSDK_SearchRequest alloc] initWithSearchString:name position:nil cc:cc kind:NSDK_SK_TOWNBYNAME_SORTED]; _mTownResults = [NSDK_Navigation searchTown:request count:MAX_TOWN_HITS mergeTowns:YES error:&error]; } - (void) searchStreet:(NSString*) name formerTownResult:(NSDK_SearchResult*) formerTownResult { [self resetStreetSearch]; NSError * error; NSDK_SearchRequest* request = [[NSDK_SearchRequest alloc] initWithSearchString:name kind:NSDK_SK_STREETBYNAME]; _mStreetResults = [NSDK_Navigation searchStreet:request count:MAX_STREET_HITS townSearchResult:formerTownResult error:&error]; } - (void) searchHNR:(NSString*) name formerTownResult:(NSDK_SearchResult*) formerTownResult formerStreetResult:(NSDK_SearchResult*) formerStreetResult { [self resetHNRSearch]; NSError * error; NSDK_SearchRequest* request = [[NSDK_SearchRequest alloc] initWithSearchString:name kind:NSDK_SK_HNR]; _mHNRResults = [NSDK_Navigation searchHouseNr:request count:MAX_HNR_HITS townSearchResult:formerTownResult streetSearchResult:formerStreetResult error:&error]; } - (int) count:(NSDK_SearchKind) kind { switch (kind) { case NSDK_SK_TOWNBYNAME_SORTED: return [_mTownResults size]; break; case NSDK_SK_STREETBYNAME: return [_mStreetResults size]; break; case NSDK_SK_HNR: return [_mHNRResults size]; break; default: return [_mTownResults size]; } } - (NSDK_SearchResult*) getResult:(int) index searchKind:(NSDK_SearchKind) kind { NSDK_SearchResults* searchResults = nil; switch (kind) { case NSDK_SK_TOWNBYNAME_SORTED: searchResults = _mTownResults; break; case NSDK_SK_STREETBYNAME: searchResults = _mStreetResults; break; case NSDK_SK_HNR: searchResults = _mHNRResults; break; } return searchResults[index]; } - (void) resetTownSearch { _mTownResults = nil; } - (void) resetStreetSearch { _mStreetResults = nil; } - (void) resetHNRSearch { _mHNRResults = nil; } @end
Make the methods visible by adding them in SearchModel.h
#import <Foundation/Foundation.h> #import "navicoreFramework/NSDK_Navigation.h" @interface SearchModel : NSObject - (instancetype) init __attribute__((unavailable("init not available, use 'sharedInstance' Method for getting the object"))); + (instancetype) sharedInstance; - (void) searchTown:(NSString*) name countryCode:(int) cc; - (void) searchStreet:(NSString*) name formerTownResult:(NSDK_SearchResult*) formerTownResult; - (void) searchHNR:(NSString*) name formerTownResult:(NSDK_SearchResult*) formerTownResult formerStreetResult:(NSDK_SearchResult*) formerStreetResult; - (int) count:(NSDK_SearchKind) type; - (NSDK_SearchResult*) getResult:(int) index searchKind:(NSDK_SearchKind) kind; - (void) resetTownSearch; - (void) resetStreetSearch; - (void) resetHNRSearch; @end
SearchViewController, the code
First, we have to wire the UIButtons and the UIMapView to properties (do this by Ctrl-Drag them vom Main.storyboard to the Header File). Furthermore add the following NSDK_SearchResult and NSDK_SearchKind properties in SearchViewController.h
#import <UIKit/UIKit.h> #import "navicoreFramework/NSDK_Navigation.h" #import "UIMapView.h" #import "Country.h" @interface SearchViewController : UIViewController <UIPickerViewDelegate, UIPickerViewDataSource> @property NSMutableArray <Country*> * countries; @property Country * mCurrentCountry; @property NSDK_SearchKind searchKind; @property (retain, nonatomic) NSDK_SearchResult * mCurrentTownResult; @property (retain, nonatomic) NSDK_SearchResult * mCurrentStreetResult; @property (retain, nonatomic) NSDK_SearchResult * mCurrentHNRResult; @property (weak, nonatomic) IBOutlet UIButton *uibuttonTown; @property (weak, nonatomic) IBOutlet UIButton *uibuttonStreet; @property (weak, nonatomic) IBOutlet UIButton *uibuttonHousenumber; @property (weak, nonatomic) IBOutlet UIMapView *uiMapView; @property (weak, nonatomic) IBOutlet UIButton *uibuttonNavigateWithSimulation; @property (weak, nonatomic) IBOutlet UIButton *uibuttonNavigate; @property (weak, nonatomic) IBOutlet UIButton *uibuttonCountry; - (void) setCurrentSearchResult:(NSDK_SearchResult*) searchResult; @end
For the complete implementation code of SearchViewController.m
, have a look at the corresponding XCode Project of this tutorial. Two functions will be described in this tutorial. First the "prepareForSegue" function:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([[segue destinationViewController] isKindOfClass:[ResultViewController class]]) { ResultViewController * resultViewController = [segue destinationViewController]; resultViewController.searchViewController = self; if ([[segue identifier] isEqualToString:@"segueSearchViewToResultViewTown"]) { _searchKind = NSDK_SK_TOWNBYNAME_SORTED; _mCurrentStreetResult = nil; _mCurrentHNRResult = nil; } else if ([[segue identifier] isEqualToString:@"segueSearchViewToResultViewStreet"]) { _searchKind = NSDK_SK_STREETBYNAME; } else if ([[segue identifier] isEqualToString:@"segueSearchViewToResultViewHNR"]) { _searchKind = NSDK_SK_HNR; } } }
The second function which is described here, is the "setCurrentSearchResult:" function which is called by the ResultViewController, when the user has selected a search result from the UITableView:
- (void) setCurrentSearchResult:(NSDK_SearchResult*) searchResult { int mapViewScale = 500; switch (_searchKind) { case NSDK_SK_TOWNBYNAME_SORTED: _mCurrentTownResult = searchResult; _mCurrentStreetResult = nil; _mCurrentHNRResult = nil; break; case NSDK_SK_STREETBYNAME: _mCurrentStreetResult = searchResult; _mCurrentHNRResult = nil; mapViewScale = 200; break; case NSDK_SK_HNR: _mCurrentHNRResult = searchResult; mapViewScale = 100; break; } [self enableButtons]; [self setButtonTitles]; //delete old pin [NSDK_Navigation deleteImages:nil]; [NSDK_Navigation addImage:@"pin.png" center:[searchResult getPosition] error:nil]; [_uiMapView setCenterWithPosition:[searchResult getPosition]]; [_uiMapView setScale:mapViewScale]; [_uiMapView refresh]; }
ResultViewController, the code
Open the Main.storyboard and create properties for the UITextField and the UITableView. Furthermore add a property for the SearchViewController and protocol implementation informations for UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource
. The Header file ResultViewController.h should look like this:
@interface ResultViewController : UIViewController <UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource> @property (retain, nonatomic) SearchViewController *searchViewController; @property (weak, nonatomic) IBOutlet UITextField *uiTextField; @property (weak, nonatomic) IBOutlet UITableView *uiTableView; @end
As you can see in the following ResultViewController.m code, the SearchModel
is used to perform the actual search.
#import "ResultViewController.h" #import "SearchModel.h" #import "SearchViewController.h" @interface ResultViewController () @property (nonatomic, retain) SearchModel * searchModel; @property BOOL searchStarted; @end @implementation ResultViewController - (void)viewDidLoad { [super viewDidLoad]; if (_searchModel == nil) { _searchModel = [SearchModel sharedInstance]; } self.uiTextField.delegate = self; self.uiTableView.delegate = self; self.uiTableView.dataSource = self; _searchStarted = NO; } - (void) viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self.uiTextField becomeFirstResponder]; } - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { _searchStarted = YES; NSString * text = [[textField text] stringByReplacingCharactersInRange:range withString:string]; switch (_searchViewController.searchKind) { case NSDK_SK_TOWNBYNAME_SORTED: [_searchModel searchTown:text countryCode:_searchViewController.mCurrentCountry.ptvCode]; break; case NSDK_SK_STREETBYNAME: [_searchModel searchStreet:text formerTownResult:_searchViewController.mCurrentTownResult]; break; case NSDK_SK_HNR: [_searchModel searchHNR:text formerTownResult:_searchViewController.mCurrentTownResult formerStreetResult:_searchViewController.mCurrentStreetResult]; break; default: break; } [_uiTableView reloadData]; return YES; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ if (!_searchStarted) { return 0; } return [_searchModel count:_searchViewController.searchKind]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ static NSString *simpleTableIdentifier = @"SimpleTableCell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleTableIdentifier]; } NSDK_SearchResult* searchResult = [_searchModel getResult:(int)indexPath.row searchKind:_searchViewController.searchKind]; cell.textLabel.text = [searchResult getName]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ NSDK_SearchResult* searchResult = [_searchModel getResult:(int)indexPath.row searchKind:_searchViewController.searchKind]; [_searchViewController setCurrentSearchResult:searchResult]; [self performSegueWithIdentifier: @"segueResultViewToSearchView" sender: self]; } @end
When the user selects an entry of the Table, [SearchViewController setCurrentSearchResult:] is called and the Exit Segue is performed.