Using OneMap Address Search in iOS App

In this tutorial, we are going to implement the address search feature using OneMap REST API. If you want to know more about OneMap, please visit to OneMap API Help page.

We will continue our tutorial from my post how to add OneMap to iOS App, in which I’ve shown how to use ArcGIS iOS SDK and adding OneMap base map and setting map extent. Complete the steps in it before attempting this tutorial.

We are going to call the Address Search REST API from OneMap form our iOS app. I’ve tried out the API call and explored the data returned from the service in previous post Exploring OneMap REST API with Swift Playgrounds.

Add Navigation Controller
First, we need to add Navigation controller to our View Controller. Go to Editor > Embed In > Navigation Controller. A Navigation Controller is added to our View Controller.

Embed in navigation

Add UISearchController And UITableViewController
We are going to use the UISearchController to allow users to key in address. The results will be shown in UITableViewController as the user type in the key words. Add UISearchController property and UITableViewController property in the View Controller.

In the viewDidLoad( ) method, add following setup code for UISearchController.

Next, add the necessary delegates to View Controller as follow:

Create a data structure to hold the address data
Create a structure named Place to hold address name, category, X and Y.

In ViewController, declare an array of Places. This array will be our data model to store the search results.

Implement UITableView delegate protocol methods 
There are three protocol methods we need to implement to adopt the UITableView delegate protocol. Add the following methods to ViewController Class.

Implement UISearchResultsUpdating delegate protocol method
UISearchResultsUpdating protocol requires to implement the following method. This allow us to update search results based on the information users enters in the search bar.

Next, we will implement the function to call OneMap REST API to search for the address.

Run the application
If everything goes well, a search bar will be displayed on the navigation controller title place.

searchbar

When the user type in the search keywords in the search bar, getAddresses function is called and a list of addresses started by the keyword is shown in the table view as suggestion.

Auto suggestion

Show the selected one on the map view
We need to implement one more function to show the search result when the user clicks on one of the suggested places. This function is called in tableView:didSelectRowAtIndexPath: delegate method.

showaddress

Above screen is shown when the user selects City Hall from suggested addresses. The project is available on github. Please leave comments if you have any questions and suggestion.

Using OneMap in iOS map application

I am learning Swift, the new programming language developed by Apple for iOS and OSX application development, and ArcGIS iOS SDK. I am planning  to make a series of tutorials based on what I have learned progressively.

In this tutorial, I am going to show you how to use Singapore’s official map platform, OneMap (www.onemap.sg), in iOS 8 application.

OneMap is an integrated map system for government agencies to deliver location-based services and information. It is a multi-agency collaboration with many government agencies currently participating and contributing information. ~ http://www.onemap.sg/home

To use OneMap in our iOS app, we need to use ArcGIS iOS SDK from Esri.  First download the ArcGIS SDK for iOS from https://developers.arcgis.com/ios/. Follow the guide to install and setup the development environment. ArcGIS has a very comprehensive guide for installing and setting up the development environment.

To use OneMap, first you need to add a UIView to your storyboard. In the Identity Inspector, change the UIView class name to AGCMapView.

MapView

Connect the map view to the view controller by creating an outlet in in ViewController.swift file.

Outlet

Import the ArcGIS framework in view controller’s swift file to use ArcGIS SDK for ios.

In the viewDidLoad method of ViewController.swift file, create a tiled map layer using OneMap map service and add it to the map:

Run the application and you will see the map application displaying onemap.sg base map in the map view.

Initial Run

Right now, our map displays the whole Singapore, but we want it to be focus on a particular area in Singapore when it is loaded. To do this we need to define the initial map extent to zoom in display. We need to pass the envelope geometry to the  zoomToEnvelope:animated: method to zoom to the area we are interested in. 

An envelope is defined by a pair of X-Y coordinates representing the lower-left and upper-right corners of a rectangular bounding-box.

To create an envelope geometry, we need X min, Y min, X max and Y max. How do we find out this values? Pretty easy. We can get the current map’s extent from mapView.visibleAreaEnvelope property. So we just have to zoom in to the area of interest and inspect the current mapView.visibleAreaEnvelope values and use them as the initial extent in viewDidLoad method.

Our View Controller must adapt to the  AGSMapViewLayerDelegate protocol and set itself as the delegate of mapView‘s layerDelegate.

In the viewDidLoad method, add this line to set delegate. See the API reference documentation for the AGSMapViewLayerDelegate protocol to learn more on layerDelegate

Then implement mapViewDidLoad delegate method to add observer for mapView pan and zoom notifications.

Run the application and zoom to the area you want to show on initial start up. As you zoom in to the area, you can see then envelope geometry data printed out in Xcode console. When you are satisfied with the view, copy the X min, Y min, X max and Y max from the console and use them as initial extent.

I choose Nanyang Polytechnic as my starting view and create an envelop geometry from it. Following is the complete code:

Mat on NYP

In this tutorial, we have learned how to add OneMap as based map to the iOS application and setting up initial map extent to the location of interest by using envelop geometry. If you want to know more about OneMap features and services, please visit to their API help page http://www.onemap.sg/api/help/.

Look forward for more tutorials on using OneMap from me and please leave a comment if you find any error or have difficulty following the steps above.

“Could not load data from the data source” error in ArcMap 10.1

When working in ArcMap, one would encounter all kinds of wired-ass problems for all kinds of unknown reasons. One of them is when you are working on a map document for a while, and out of the blue, unable to open the attribute table of a layer or layers in the document.

The error usually occurs when you right click on a layer and click “Open attribute table”. Instead of showing the attribute table, ArcMap displays you the following message and an empty attribute table. The strange thing is you still can select the rows in the attribute table and you can see how many rows in the table as well. It is just that all the rows are now empty.

couldnotload

empty_table

There are a few ways to solve this problem. One easy way, which worked for me, is to export out the problematic layer to a new geodatabase and then import it back to your map document. I hope this is helpful to those who encounters similar problem using ArcMap.

One or more layers failed to draw: ArcMap Drawing Errors

I have been working with a huge map document with thousands of layers, and encountered this weird error where by if I tried to zoom out, one of the layers disappeared. If I zoomed in, it appeared again.

I have tried many solutions I could google out for a few days, but nothing worked. I put my problem on geonet.esri.com discussion board and waited for the help. To my surprise, one user responded my question with a few precise suggestions and my problem has solved. The solution which worked for me is simply to copy the feature layers over to new map session.

Following is the link to the thread on geonet.esri.com where I got the help. 🙂
https://geonet.esri.com/message/451155#451155

Using arcpy for geoprocessing

This tutorial will show you how to use arcpy to do basic geoprocessing tasks.

I have a feature class of private properties with attributes such as property’s name, address, latitude, longitude and completion year, i.e  the year it was completed development.

Unfortunately in some of the records, the completion year value is missing. Now, I will show you how to estimate the missing data by using geoprocessing with arcpy.

Following illustration shows the process flow I used to achieve this task.

estimate_workflow

Extract Records with missing completion year

First I need to extract those records with missing year from the feature class. The records are stored in “C:\\workspace\default.gdb” as “PRIVATEHOUSES” feature class.


import arcpy
arcpy.env.workspace ='C:\\workspace\\default.gdb'
input_feature = 'PRIVATEHOUSES'

# First make a layer from feature class. Because we are going to use
# arcpy.SelectLayerByAttribute_management tool and it doesn’t allow
# feature classas input

arcpy.MakeFeatureLayer_management(input_feature, 'input_lyr')

# This will select the records from input_lyr with
# Comletion_Date value is Null

mising_records = arcpy.SelectLayerByAttribute_management('input_lyr',
                                                          'NEW_SELECTION',
                                                          'Completion_Date IS Null')

This code snapshot shows you how to use SelectLayerByAttribute_management tool to select records based on attribute value of feature class or layer. After running this code, I have all the records with missing year value in “missing_records” variable.

Search for other records within specified range

Next, I need to loop through all missing_records and get estimation for each of them.

# Create a search cursor to loop through records
rows = arcpy.SearchCursor(missing_records)
for row in rows:
    #  Estimation code goes here
    #  …
    #  …

arcpy.SearchCursor() creates a read-only access cursor to records of a feature class or table. For each record, I try to find the surrounding data points within specified range __ in this case, I use 100 meters.

# Get object id from the search cursor
objectid = row.getValue("OBJECTID")
query = "OBJECTID=%d" % objectid

# Selecting the feature with the current OBJECTID
temp_selected = arcpy.SelectLayerByAttribute_management('ph_lyr', "NEW_SELECTION", query )

# Selecting surrounding records within 100 meters
selected_within_100m = arcpy.SelectLayerByLocation_management('ph_lyr',
                                                              'WITHIN_A_DISTANCE',
                                                               temp_selected,
                                                               100,
                                                               "NEW_SELECTION")

I extract the object id of the current record by using row.getValue() method. After that, the query is prepare for selecting current feature. Then I use arcpy.SelectLayerByAttribute_management(…) to select the current feature and assign it into variable temp_selected. This variable is passed to arcpy.SelectLayerByLocation(…) function to get the records in specified range.

Get a median completion year

Next step is to get estimation of completion year from the retrieved records within range. For estimation I just calculate and return median value of the other records in range.

# Make an estimation of year of completion based on surrounding records
def getEstimateFromSurroundings(surrounding_rows):
    surrounding_years =[]
    # Ignore records with null values
    for row in surrounding_rows:
        val = row.getValue("Completion_Date")
        if val != None:
            surrounding_years.append(val)

    # if there is no values in the list, return None
    if len(surrounding_years) == 0:
        return None

    # I use numpy library to calculate median
    median_year = numpy.median(surrounding_years)
    return median_year

I write a function called getEstimateFromSurroundings() function to do the estimation work. In the function, first, I check and remove the records with Null value. The rest are put into a list and median value is extracted. I use numpy to calculate median value from the list, you can implement your own function to get it but I am too lazy do it myself 🙂

After I got the estimation, I update the current record completion year with the estimated value. Since I don’t want to modify the input feature layer, I make a copy of the input feature layer for output using CopyFeatures_management and update the estimated value to it.

# Copy the input feture
arcpy.CopyFeatures_management(input_feature, output_feature)

Update the missing completion year with estimated one

Next I implement a function to update estimated value to current record.

def updateCompletionYear(objectid, year):
    update_cursor = arcpy.UpdateCursor(output_feature, "OBJECTID=%d" % objectid)
    if(update_cursor):
        update_row = update_cursor.next()
        update_row.Completion_Date = year
        update_cursor.updateRow(update_row)

        # Release pointer to update record
        del update_row

    # Release pointer to update cursor
    del update_cursor

The function above do the updating of record to output feature layer for the given OBJECTID and year value. I use arcpy.UpdateCursor(…) method to update the Completion_Year data field.

Putting all together

# To estimate completion year from surrounding units

import arcpy
import numpy
from datetime import datetime

arcpy.env.workspace = "C:\\workspace\\default.gdb"
arcpy.env.overwriteOutput = True

input_feature = "PRIVATEHOUSES"
output_feature = "OUTPUT_PRIVATEHOUSES"

# Make an estimation of year of completion based on surrounding houese
def getEstimateFromSurroundings(surrounding_rows):
	surrounding_years =[]
	for row in surrounding_rows:
		val = row.getValue("Completion_Date")
		if val != None:
			surrounding_years.append(val)

	if len(surrounding_years) == 0:
		return None

	median_year = numpy.median(surrounding_years)
	#print surrounding_years
	#print "Estimated age: ", median_year
	return median_year

def updateCompletionYear(objectid, year):
	update_cursor = arcpy.UpdateCursor(output_feature, "OBJECTID=%d" % objectid)
	if(update_cursor):
		update_row = update_cursor.next()
		update_row.Completion_Date = year
		update_cursor.updateRow(update_row)
		del update_row
	del update_cursor

def main():
	# Timing
	starttime = datetime.now()

	# First make a layer from feature class
	# Because select layer by attribute doesn't work with feature class
	arcpy.MakeFeatureLayer_management(input_feature, 'ph_lyr')
	arcpy.CopyFeatures_management(input_feature, output_feature)
	uncomplete_records = arcpy.SelectLayerByAttribute_management('ph_lyr', "NEW_SELECTION", "Completion_Date IS Null")
	rows = arcpy.SearchCursor(uncomplete_records)
	for row in rows:
		objectid = row.getValue("OBJECTID")
		query = "OBJECTID=%d" % objectid
		temp_selected = arcpy.SelectLayerByAttribute_management('ph_lyr', "NEW_SELECTION", query )
		selected_within_40m = arcpy.SelectLayerByLocation_management('ph_lyr', 'WITHIN_A_DISTANCE', temp_selected, 1200, "NEW_SELECTION")
		surrounding_rows = arcpy.SearchCursor(selected_within_40m)
		estimated_year = getEstimateFromSurroundings(surrounding_rows)

		updateCompletionYear(objectid, estimated_year)

		if estimated_year == None:
			estimated_year = -1
		print "Objectid %d, year %d" % (objectid, estimated_year)
	print (datetime.now() - starttime)

if __name__ == '__main__':
	main()

This process takes place for all the records in the for loop. After finished, all the records in my output feature layer has completion year.