Skip navigation
All People > jjackson-esristaff > Developing with ArcGIS

While I've used several ArcGIS APIs over the years, I'm relatively new to web development. I recently started developing a few apps that could be characterized as ambitious, so naturally I turned to Ember.js.

 

With a lot of experience developing for iOS and before that .NET, I've embraced MVC and MVVM. EmberJS is MVC done right. In fact, I find it very similar to the MVVM framework we developed for some of our Windows and Silverlight products.

 

EmberJS is fantastic. The more I work with it, the more I appreciate it. So naturally I tried to add a map to an app using our JavaScript API. It did not go well at first. Luckily, with some guidance from my esteemed colleague Frederic Aubry, I began to make sense of AMD (asynchronous module definition) and can now get the lego blocks to connect nicely.

 

I'm going to assume you already know Ember.js. There are great tutorials out there. My personal favorite is the video on the on the website Ember.js - Guides and Tutorials: Ember.js Guides . I learned a lot by pausing the video, typing in the code and experimenting. Another good tutorial, also on their site, is called Getting Started and walks you through building the TodoMVC demo application.

 

 

Getting Started

The easiest way to get started is to pull the starter kit for Ember emberjs/starter-kit · GitHub . Rename the folder to ember-arcgis-sample.

 

The first task is package up the logic of the app into modules. As it is now, the App class is simply a variable that is declared globally. To make use of ArcGIS functionality that comes prepackaged in modules, it's easier to use the same approach throughout. You can find a nice overview of AMD modules, module loading, packages, and more here -Introduction to AMD Modules - The Dojo Toolkit .

 

We will put all of our javascript modules into a single package and call it sample. Create a folder inside the js folder and name it sample. Then move app.js into the sample folder. We'll come back to the app.js file shortly.

 

Open index.html and add the following right after the <body> element:

 

<script>

     dojoConfig= {

        parseOnLoad: false,

        packages: [

            {"name":"sample", "location": location.pathname.replace(/\/[^/]+$/, "") + "/js/sample"}

        ]

     };

</script>

<script src="//js.arcgis.com/3.13/"></script>

 

This first script defines a variable dojoConfig that configures the dojo loader which is used by the ArcGIS API to load modules. It specifies a new package that will be available to the loader - our local package, sample. The second script loads the dojo module loader which is contained in the ArcGIS JavaScript API.

 

Next move the three scripts that load jQuery and Ember after the loader configuration script. Then add the following line:

 

<script src="js/libs/jquery-1.10.2.js"></script>

<script src="js/libs/ember-template-compiler-1.10.0.js"></script>

<script src="js/libs/ember-1.10.0.debug.js"></script>

 

<script src="run.js"></script>

 

The new script - run.js - will act as the initial entry point for our app. We'll look at that next. Note that the two handlebars scripts are now at the bottom of the html body. In a future post I will show how these can be removed from index.html altogether.

 

 

Create run.js

Create a file in the root folder of the project - ember-arcgis-sample - called run.js. We are going to move the Ember.js route definitions from app.js into run.js.

 

require([

   "sample/app"
], function (App) {

 

  // define the routes
  //
  App.IndexRoute = Ember.Route.extend({

        model: function() {

             return ['red', 'yellow', 'blue'];

       }

  });

  

  // start the router
  //
  App.Router.map(function() {

        // put your routes here
  });

 

  // we are now ready and can let the app
  // start running
  //
  App.advanceReadiness();

});

 

The thing to notice about this script is that it's wrapped by a call to require(). The require statement takes an array of modules as the first parameter. Note that the module we are requiring is the app module within the sample package. The second parameter to require is the function that will be called once every one of the requirements has been met - that is, once each of the modules has been loaded.

 

The function takes a series of parameters. Each parameter corresponds to the return value from the module that was passed in the first array parameter. So in our case, the app module will create the App instance and return it. That instance will be passed to us as the App parameter (you could name it anything you want).

 

The AMD loader will cache the return values of the modules it loads, so as we create additional modules that depend on the app module, we can be sure that the App instance will only be created once.

 

 

Modify app.js

All that is left of app.js is creating the App instance. But since we are turning app.js into a module, we need to modify it to use the define statement.

 

define(function () {

 

   var isDebug = true;

   var App = Ember.Application.create({

        LOG_TRANSITIONS: isDebug,

        LOG_TRANSITIONS_INTERNAL: isDebug,

        LOG_STACKTRACE_ON_DEPRECATION: isDebug
   });

   App.deferReadiness();

 

   return App;

});

 

The define statement defines a module. The module is contained in a function with an optional return value. In our case the module simply creates the Ember Application instance. We call deferReadiness() to delay the initialization process. This will become important later on when we are have more functionality contained in modules that will be loaded asynchronously.

 

Note that in the previous step when you created run.js you included a call to advanceReadiness() to continue the app initialization process that is delayed when the app is first created.

 

At this point you can run the app and you'll see the same results as before.

 

Add a Map

The last step is to add a map to your app. Modify run.js and add a new route to your app called map:

 

App.Router.map(function() {

   this.resource("map");

});

Then modify the IndexRoute. Remove the model and add a transition to the new map route:

 

App.IndexRoute = Ember.Route.extend({

        beforeModel: function (transition) {

             this.transitionTo("map");

       }

});

 

Add a new file to the sample folder called map-view.js.

 

define([

   "sample/app",

   "esri/arcgis/utils"
], function (App, arcgisUtils) {

 

  App.MapView = Ember.View.extend({

 

       onDidInsertElement: function() {

 

            var mapDiv = document.createElement("div");

            mapDiv.id = "map";

            this.element.appendChild(mapDiv);

 

            arcgisUtils.createMap("22d8f8438f294b3e8710bbade31b4ee6", "map").then(function(response) {

                 response.map.resize();

            });

 

       }.on("didInsertElement")

   });

});

 

The map-view module is dependent on the app module and the esri/arcgis/utils module. When it loads, it creates a new a new class - MapView - and adds it to the App instance. The meat of the MapView occurs when the corresponding DOM element is inserted. When that happens, a child div is added to the element and then we let the ArcGIS API create a Map corresponding to the specified webmap. You can see that webmap here http://nitro.maps.arcgis.com/home/webmap/viewer.html?webmap=22d8f8438f294b3e8710bbade31b4ee6

 

Next, you need to load the map-view module so that the MapView class can be defined. The simplest way to do that is to add sample/map-view as a requirement to the run.js script:

 

require([

     "sample/app",

     "sample/map-view"
], function (App) {

 

Finally, add a link to the arcgis stylesheet inside the <head> element of index.html.

 

<head>

   <meta charset="utf-8">

   <title>Ember ArcGIS Sample</title>

   <link rel="stylesheet" href="css/normalize.css">

   <link rel="stylesheet" href="css/style.css">

    <link rel="stylesheet" href="//js.arcgis.com/3.13/esri/css/esri.css">

</head>

 

That's it. Run your application and you should see a map!Screen Shot 2015-03-25 at 8.59.39 AM.png

You can find the source code of the completed app here jeffjax/ember-arcgis-sample · GitHub

One of the nifty newer features of the ArcGIS world geocoding service is the ability to provide search suggestions very quickly. Apps like Explorer for ArcGIS use this feature to provide suggestions as the user types into the search bar. Currently, the iOS Runtime SDK doesn’t expose the new capability, but it’s still fairly straight forward to add suggestions to your app.

 

IMG_2344.png

 

Extending the Locator

The AGSLocator class lets you geocode addresses, find places, and reverse-geocode locations. It provides a client side abstraction to interact with the world geocoding service hosted on ArcGIS Online or on-premise with ArcGIS Server. It's this service that now offers the suggest endpoint. 

 

The first step to extending AGSLocator is to create a Swift file in your project. You can name this AGSLocator.swift. You'll need to import the ArcGIS framework, then define an extension to hold the new methods.

 

import ArcGIS

 

extension AGSLocator {

 

}

 

The first thing we need to tackle is the suggest endpoint. The suggest endpoint lets you provide text and some optional parameters like location and distance, then returns back suggestions. For the full description of the REST endpoint check out suggest—ArcGIS REST API: World Geocoding Service | ArcGIS for Developers.

 

The approach we are going to take is to use an AGSJSONRequestOperation which is a convenience object provided by the SDK to simplify the process of making a web service request that passes along JSON. A large part of the iOS Runtime SDK makes use of this class. The body of our method will look something like this:

 

        let paramsDictionary = NSMutableDictionary(objectsAndKeys: "json", "f")

        paramsDictionary.addEntriesFromDictionary(params.encodeToJSON())

     

        let op = AGSJSONRequestOperation(URL: self.URL, resource: "suggest", queryParameters: paramsDictionary)

        op.securedResource = self

        op.requestCachePolicy = self.requestCachePolicy

        op.timeoutInterval = self.timeoutInterval

     

        op.completionHandler = { (obj: AnyObject!) -> Void in

            // TODO - deal with the json result, a dictionary

        }

     

        op.errorHandler = { (err: NSError!) -> Void in

            // TODO - deal with the error

        }

     

        // queue up the operation

        //

        AGSRequestOperation.sharedOperationQueue().addOperation(op)

 

The first step is to take the parameters which will be the same AGSLocatorFindParameters class used for other AGSLocator methods and serialize them to JSON so we can pass them to the AGSJSONRequestOperation initializer. Then we copy over the settings of the AGSLocator class (self) to the operation and setup the completion handlers. Finally we queue up the operation.

 

So now we need to decide how clients will call this new method. Since it's an asynchronous call we need to choose a pattern for the callback. The rest of the AGSLocator class uses a delegation pattern, but to be more Swift-like, we can use a closure.

 

The other thing we need to consider is the results of the call. Notice that the completionHandler gets passed a generic object (AnyObject) which is actually an NSDictionary containing the parsed JSON payload. Clients probably don't want to deal with that directly so let's introduce an abstraction that represents a suggestion, AGSLocatorSuggestion.

 

class AGSLocatorSuggestion : NSObject, AGSCoding

    var text: String!

    var magicKey: String!

    var isCollection : Bool = false

}

 

Note that our new class conforms to the AGSCoding protocol. I left out the implementation of those methods which deal with encoding and decoding JSON. With this new class to represent the result, we can define our new suggest method like this:

 

    func suggestionsForParameters(params: AGSLocatorFindParameters, completion: (results: [AGSLocatorSuggestion]?, error: NSError?) -> Void)

 

Clients simply provide a params object and a closure and the closure gets called with either an array of suggestions, or an error. Here's the full source for the method:

 

    func suggestionsForParameters(params: AGSLocatorFindParameters, completion: (results: [AGSLocatorSuggestion]?, error: NSError?) -> Void) {

     

        let paramsDictionary = NSMutableDictionary(objectsAndKeys: "json", "f")

        paramsDictionary.addEntriesFromDictionary(params.encodeToJSON())

     

        let op = AGSJSONRequestOperation(URL: self.URL, resource: "suggest", queryParameters: paramsDictionary)

        op.securedResource = self

        op.requestCachePolicy = self.requestCachePolicy

        op.timeoutInterval = self.timeoutInterval

     

        op.completionHandler = { (obj: AnyObject!) -> Void in

 

            var suggestions = [AGSLocatorSuggestion]()

            if let json = obj as? NSDictionary {

                if let array = json["suggestions"] as? [NSDictionary] {

                    for suggestionJson in array {

                        suggestions.append(AGSLocatorSuggestion(JSON: suggestionJson))

                    }

                }

            }

            completion(results: suggestions, error: nil)

        }

     

        op.errorHandler = { (err: NSError!) -> Void in

            completion(results: nil, error: err)

        }

     

        AGSRequestOperation.sharedOperationQueue().addOperation(op)

    }

 

I've added the code in the completion handler that decodes the JSON dictionary using the AGSLocatorSuggestion initializer.

 

Using the New Method

To use this new AGSLocator method the client simply needs to create an AGSLocatorFindParams object, populate it, then pass it in:

 

    func findSuggestions(searchText: String) {

        let params = AGSLocatorFindParameters()

        params.text = searchText

        params.maxLocations = 5

        params.outFields = ["*"]

        params.location = mapView.visibleAreaEnvelope.center

        params.distance = 5000

      

        lastSearch = searchText

      

        locator.suggestionsForParameters(params, { (results: [AGSLocatorSuggestion]?, error: NSError?) -> Void in

            self.suggestions = results

            self.tableView.reloadData()

        })

    }

 

Here we are not only providing the text for the suggestions, but also some information about the geographic context - namely the center point of the map and a radius of 5 kilometers. That information will help the geocoding service tailor the results based on what we're currently looking at.

 

What is a Suggestion?

The results returned are AGSLocatorSuggestion instances. The class that we introduced has three properties - text, magicKey, and isCollection. The text is the human readable text that you can use to show the suggestion to a user. In the example above the text from the first suggestion is "Gotham, England, United Kingdom".

 

The magicKey is a unique identifier provided by the server so that it can quickly retrieve the corresponding result at a later time. We'll see how we use this shortly.

 

The isCollection property indicates if the suggestion represents a specific place or a collection of places. If isCollection is false, the suggestion represents a single place like 100 Commerical St., Portland, ME. If isCollection is true it means the suggestion represents a search term for a common place name or a category of things. For example, the suggestion for a term like Coffee will likely be a collection of nearby coffee shops.

 

So retrieving the suggestions is the first step. The next step is to use one of the suggestions to actually retrieve locations.

 

Finding Locations with a Suggestion

Using the suggestion result to find locations is relatively simple. We can use the existing method, findWithParameters. The challenge is that we need to augment the AGSLocatorFindParameters class to introduce a new property, the magicKey. When the service responds to a find request and the magicKey is included in the parameters, it can quickly find the result that it cached when we made the call to suggest. So how do we add the magicKey to the existing class?

 

The most straight forward approach is to derive a new class that adds the magicKey property. The key is to override the JSON serialization methods so that when the AGSLocator prepares the operation corresponding to the find request, all of the appropriate values are present.

 

class AGSLocatorMagicFindParameters : AGSLocatorFindParameters {

   

    var magicKey : String!

   

    init() {

        super.init()

    }

   

    override func decodeWithJSON(json: [NSObject : AnyObject]!) {

        super.decodeWithJSON(json)

        magicKey = json["magicKey"]! as String

    }

   

    override func encodeToJSON() -> [NSObject : AnyObject]! {

        var result = NSMutableDictionary(dictionary: super.encodeToJSON())

        result["magicKey"] = magicKey

        return result

    }

}

 

Now, when we use findWithParameters we can include an AGSLocatorMagicFindParameters instance instead of the more mundane AGSLocatorFindParameters.

 

        let params = AGSLocatorMagicFindParameters()

        params.magicKey = suggestion.magicKey

        params.text = suggestion.text

        params.outSpatialReference = mapView.spatialReference

        params.maxLocations = suggestion.isCollection ? 5 : 1

       

        locator.delegate = self

        locator.findWithParameters(params)

 

Note that when we use the locator here we are using the more traditional delegation pattern. I'll leave it as an exercise for the reader to enhance the AGSLocator extension with a version of findWithParameters that takes a Swift closure instead. (Or maybe that can be the subject of another blog post.)

 

Conclusion

That's pretty much it. Once you retrieve AGSLocatorFindResult objects from findWithParams it's no different that before. If you'd like to see this in a working app, checkout my swift-samples repo here: jeffjax/swift-samples · GitHub

 

The sample is called Suggest. Let me know if you have any suggestions of your own.

jjackson-esristaff

Old School Enums

Posted by jjackson-esristaff Employee Jul 22, 2014

When working with the ArcGIS Runtime SDK and Swift we are occasionally reminded that the SDK was written in Objective-C. And while Swift generally does a nice job of bridging the two languages, there are some shortcomings. One of those is how Swift deals with enums.

 

For example, the enum AGSGeometryOffset type is defined in Objective-C like this:

 

typedef enum {

  AGSGeometryOffsetTypeMitered = 0,   /*!< Mietered */

  AGSGeometryOffsetTypeBevelled,      /*!< Bevelled  */

  AGSGeometryOffsetTypeRounded,       /*!< Rounded */

  AGSGeometryOffsetTypeSquare         /**  Square  */

} AGSGeometryOffsetType;

 

Swift turns that into this:

 

var AGSGeometryOffsetTypeMitered: AGSGeometryOffsetType { get }

/*!< Bevelled  */

var AGSGeometryOffsetTypeBevelled: AGSGeometryOffsetType { get }

/*!< Rounded */

var AGSGeometryOffsetTypeRounded: AGSGeometryOffsetType { get }

var AGSGeometryOffsetTypeSquare: AGSGeometryOffsetType { get } /**  Square  */

struct AGSGeometryOffsetType {

    init(_ value: UInt32)

    var value: UInt32

}

 

Note: If the Objective-C enum is defined with the NS_ENUM() macro, then Swift does a nice job of importing it; however, that's not the case for now with the ArcGIS SDK. We will be releasing an update to address this issue, but in the meantime there are some things you can do.

 

Before Beta-4, you were able to use the value property of the enum in order to do comparisons or switch statements. For example:

 

    let maneuver = someFunction()

   

    if maneuver.value == AGSGeometryOffsetTypeSquare.value {

        // do something

    }

 

But with Beta-4 which was released yesterday, that value property is no longer accessible. The error looks like this:

 

'AGSGeometryOffsetType' does not have a member named 'value'.

 

With some help from some developers on the Apple forums, I came up with this work-around. It's what I'm using in my Swift apps until we have support for the NS_ENUM() macro in the SDK. Use an extension on the enum type to introduce a new property that uses reinterpretCast to generate an integer value:

 

extension AGSGeometryOffsetType {

    var rawValue : UInt32 { get {

        var raw: UInt32 = reinterpretCast(self)

        return raw

    }}

}

 

Then use the property explicitly as before:

 

    let maneuver = someFunction()

   

    if maneuver.rawValue == AGSGeometryOffsetTypeSquare.rawValue {

        // do something

    }

 

It's no fun having to create all of those Swift extensions, but it's better than being stuck in the water until the SDK update is available.

Those of us in Esri that build apps for iOS and OS X are pretty excited about Apple's new programming language. Like you, we are trying our best to stay on top of the moving target that is Swift. I figure I can share some of our experiences to the wider ArcGIS developer community and hopefully spark some good discussion along the way.

 

Let's start out with a very simple tutorial that will get you started with an iOS app that uses Swift and ArcGIS. I'll follow up with some other posts which dive into more technical depth.

 

Augment the SDK

The ArcGIS SDK is written in Objective-C, but it works pretty well with Swift as is. In order to use it, the simplest thing you can do is add a module definition file to your SDK installation. The file should look like this:

 

framework module ArcGIS {

  umbrella header "ArcGIS.h"

  export *

  module * { export * }

}

 

The file should be named module.modulemap and it needs to be placed in a folder called Modules inside the ArcGIS.framework folder. If you installed the SDK in the default location that would be ~/Library/SDKs/ArcGIS/iOS/ArcGIS.framework/Modules. Note that you'll need to create the Modules folder.

 

You'll only need to create that file once and it will work for any Swift projects you create going forward.

 

Create a Swift Project

Start Xcode (I'm currently using Beta-4) and create a new iOS Application project. Choose the Single View Application.

 

In the project settings, add these values so that Xcode can find the ArcGIS SDK:

 

Other Linker Flags: -framework ArcGIS -ObjC -l c++

Framework Search Paths: ~/Library/SDKs/ArcGIS/iOS

 

Add a Map View

In the Main.storyboard add a view.

 

Size this view to fill its superview, and then set then add four constraints to pin the new view to the superview.

 

Show the identity inspector and change the class of the new view to be AGSMapView. Now open ViewController.swift in the assistant editor and create an outlet for the map view (Control-drag from the storyboard to the ViewController class.) If you did everything right Xcode should create a var that looks like this:

 

@IBOutlet weak var mapView: AGSMapView!

 

To fix the warning, change the import statement from UIKit to ArcGIS.

 

Now, to display some data in the map view, make the following changes to viewDidLoad():

 

    override func viewDidLoad() {

        super.viewDidLoad()

      

        let layer = AGSTiledMapServiceLayer(URL: NSURL(string: "http://services.arcgisonline.com/arcgis/rest/services/World_Topo_Map/MapServer"))

        mapView.addMapLayer(layer)

 

        let center = AGSPoint.pointWithX(-7822565, y: 5413016, spatialReference: nil)

        mapView.zoomToScale(72112, withCenterPoint:center, animated: true)

    }

 

 

The last step is to build and run your app. That's it. You're living the Swift lifestyle now.