Skip to content

04 Searching

Overview

This tutorial will teach you how to search for locations with adresses or parts of it.

Base

You can use your results from Tutorial03 as a base for our new project.

Search basics

The NavigationSDK 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] Search a complete given address and returns it's position -
[NSDK_Navigation simpleInvGeoCode] Search an address of a given position -

As you can see, some of the searches like street search depend on previous search results. You can for example only search a street in a previous found town and you can only search house numbers in a known street of a known town.

Every search is built up by the following steps:

  • Fill a search request with your search criteria
  • Call the appropriate search function with the request as a parameter
  • Get a single generic result by calling NSDK_SearchResult * result = [NSDK_SearchResults objectAtIndexedSubscript: ] with the desired 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 SearchViewController. This will be explained in one of the next tutorials. 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 we take a more detailed view of the previous code:

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: An optional position around which the search should take place. If set to "nil" we search alphabetically, not in a radius around the position. 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, all contries of the currently open map will be searched.
  • SearchKind: The kind of search. We want e.g. a town search result which orders the towns in a reasonable way (biggest towns first, then districts of towns) so we set it to SearchKind.SDK_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 geographical 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);
As you can see in the code above, we print out the results name and the geodecimal position of the result.

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.

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.

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 number of 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;

@property (nonatomic, retain) NSDK_SearchResults * currentSelectedTownResults;
@property (nonatomic, retain) NSDK_SearchResults * currentSelectedStreetResults;
@property (nonatomic, retain) NSDK_SearchResults * currentSelectedHNRResults;

@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 {

    NSError * error;
    NSDK_SearchRequest* request = [[NSDK_SearchRequest alloc] initWithSearchString:name position:nil cc:_mCurrentCountry.ptvCode  kind:NSDK_SK_TOWNBYNAME_SORTED];

    _mTownResults = [NSDK_Navigation searchTown:request count:MAX_TOWN_HITS mergeTowns:YES error:&error];
    NSLog(@"SearchModel _mTownResultMeta count:%d error:%@", [_mTownResults size], error);
}


- (void) searchStreet:(NSString*) name {

    NSError * error;
    NSDK_SearchRequest* request = [[NSDK_SearchRequest alloc] initWithSearchString:name kind:NSDK_SK_STREETBYNAME];
    _mStreetResults = [NSDK_Navigation searchStreet:request count:MAX_STREET_HITS townSearchResult:_mCurrentTownResult error:&error];
}


- (void) searchHNR:(NSString*) name {

    NSError * error;
    NSDK_SearchRequest* request = [[NSDK_SearchRequest alloc] initWithSearchString:name kind:NSDK_SK_HNR];
    _mHNRResults = [NSDK_Navigation searchHouseNr:request count:MAX_HNR_HITS townSearchResult:_mCurrentTownResult streetSearchResult:_mCurrentStreetResult 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 {

    NSDK_SearchResults* searchResults = nil;

    switch (_currentSearchKind) {
        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) setSelectedSearchResult:(int) index  searchKind:(NSDK_SearchKind) kind {

    switch (kind) {
        case NSDK_SK_TOWNBYNAME_SORTED:
            self.currentSelectedTownResults = _mTownResults;
            self.mCurrentTownResult = _mTownResults[index];
            self.mCurrentStreetResult = nil;
            self.mCurrentHNRResult = nil;
            break;
        case NSDK_SK_STREETBYNAME:
            self.currentSelectedStreetResults = _mStreetResults;
            self.mCurrentStreetResult = _mStreetResults[index];
            self.mCurrentHNRResult = nil;
            break;
        case NSDK_SK_HNR:
            self.currentSelectedHNRResults = _mHNRResults;
            self.mCurrentHNRResult = _mHNRResults[index];
            break;
    }
}


- (void) resetSearch {
    _mTownResults = nil;
    _mStreetResults = nil;
    _mHNRResults = nil;

    _mCurrentTownResult = nil;
    _mCurrentStreetResult = nil;
    _mCurrentHNRResult = nil;
}



- (NSDK_SearchResult*) getCurrentBestSearchResult {
    if (_mCurrentHNRResult != nil) {
        return _mCurrentHNRResult;
    }
    if (_mCurrentStreetResult != nil){
        return _mCurrentStreetResult;
    }
    return _mCurrentTownResult;
}


- (void) doSearch:(NSString*) text {
    switch (_currentSearchKind) {
        case NSDK_SK_TOWNBYNAME_SORTED:
            [self searchTown:text];
            break;
        case NSDK_SK_STREETBYNAME:
            [self searchStreet:text];
            break;
        case NSDK_SK_HNR:
            [self searchHNR:text];
            break;
        default:
            break;
    }
}

@end

@end
As you can see, the current NSDK_SearchResults are stored in properties (mTownResults, mStreetResults, mHNRResults). And when the user taps on a result address we store the NSDK_SearchResults in the properties currentSelectedTownResults, currentSelectedStreetResults and currentSelectedHNRResults.

Important

The latter is very important because otherwise the Framework would automatically free the memory of the search result, which would result in an access violation error when trying to access a single NSDK_SearchResult element.

Make the methods visible by adding them in SearchModel.h

#import <Foundation/Foundation.h>
#import "navicoreFramework/NSDK_Navigation.h"
#import "Country.h"

@interface SearchModel : NSObject

@property NSDK_SearchKind currentSearchKind;
@property (retain, nonatomic) NSDK_SearchResult * mCurrentTownResult;
@property (retain, nonatomic) NSDK_SearchResult * mCurrentStreetResult;
@property (retain, nonatomic) NSDK_SearchResult * mCurrentHNRResult;
@property Country * mCurrentCountry;


- (instancetype) init __attribute__((unavailable("init not available, use 'sharedInstance' Method for getting the object")));
+ (instancetype) sharedInstance;
- (void) doSearch:(NSString*) text;
- (int) count:(NSDK_SearchKind) type;
- (NSDK_SearchResult*) getResult:(int) index;
- (void) setSelectedSearchResult:(int) index  searchKind:(NSDK_SearchKind) kind;
- (void) resetSearch;
- (NSDK_SearchResult*) getCurrentBestSearchResult;
@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 (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 *uibuttonCountry;
- (void) refreshCurrentSearchResult;
@end

For the complete implementation code of SearchViewController.m, have a look at the corresponding XCode Project of this tutorial. In this tutorial we show only 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"]) {
            _searchModel.currentSearchKind = NSDK_SK_TOWNBYNAME_SORTED;
        } else if ([[segue identifier] isEqualToString:@"segueSearchViewToResultViewStreet"]) {
            _searchModel.currentSearchKind = NSDK_SK_STREETBYNAME;
        }  else if ([[segue identifier] isEqualToString:@"segueSearchViewToResultViewHNR"]) {
            _searchModel.currentSearchKind = NSDK_SK_HNR;
        }
    }
}
This function is called by the runtime, when a segue will be triggered. In this function, we look at which segue will be triggered (which button has been pressed) and set the 'currentSearchKind' in the SearchModel.

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];
    [_searchModel doSearch:text];
    [_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];
    cell.textLabel.text = [searchResult getName];
    return cell;
}


- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
  [_searchModel setSelectedSearchResult:indexPath.row  searchKind:_searchModel.currentSearchKind];
  [_searchViewController refreshCurrentSearchResult];
  [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.