Getting Started with the ArcGIS for Windows Mobile SDK

My initial thoughts on working with this SDK reminded me of the first time I worked with ArcObjects; lots of power but it can be daunting knowing where to begin. I’ve put together this post to aid developers looking to utilise this SDK and hit the ground running when building applications for Windows Mobile 6 on rugged devices.

Version Control

The starting point for a lot of people will be creating a new project which raises the question of what version of Visual Studio and the .NET Compact Framework to use? Well unfortunately VS2010 isn’t supported so that leaves us with VS2008. For the compact framework you get the choice of either 2.0 or 3.5. The ArcGIS Mobile SDK is designed for working with 2.0 but you can use 3.5, the downside being that the Visual Studio design mode won’t show the ArcGIS integration. This means that you can’t use the toolbox and designer to create your UI, not that big a deal really as I find the VS UI tooling pretty horrible anyway. If you use 3.5 you will need to create your controls using code when your form or user control is initialised. For the map you may do something like

public static class MapHelper 
{ 
    private const double MinScale = 1000; 
 
    public static Map BuildMapControl(int width, int height) 
    { 
        if (width < 1) 
            throw new ArgumentOutOfRangeException("width", "Width must be greater than zero.");  
 
        if (height < 1) 
            throw new ArgumentOutOfRangeException("height", "Height must be greater than zero."); 
 
        var map = new Map
        { 
            Location = new Point(0, 0), 
            Size = new Size(width, height), 
            Anchor = 
              System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right | 
              System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Top, 
            TabIndex = 0 
        }; 
 
        new ScaleBar 
        { 
            Map = map, 
            DisplayPosition = ScaleBarDisplayPosition.TopLeft, 
            UnitSystem = ScaleBarUnitSystem.Metric, 
            Width = 200, 
            BarColor = Color.Black, 
            HaloColor = Color.White 
        };
 
        // prevent the map zooming in too far 
        map.ExtentChanged += (sender, e) => 
        { 
            var mapCtrl = sender as Map;  
            if (mapCtrl == null) 
                return; 
 
            if (mapCtrl.Scale < MinScale) 
                mapCtrl.Scale = MinScale; 
        }; 
 
        return map; 
    } 
}

Then in your form constructor you would call

ESRI.ArcGIS.Mobile.Map map = MapHelper.BuildMapControl(480, 500); 
this.Controls.Add(map); 
 
// Define the map actions that can be used to interact with the map control 
var panMapAction = new PanMapAction(); 
map.MapActions.Add(panMapAction);
 
map.CurrentMapAction = panMapAction;

Working with Data

The most common data format that you will be working with is the MobileCache. When we work with the newer web APIs we think of the term cache as meaning pre cooked image tiles commonly used for fast rendering of complex symbology but in the Windows Mobile SDK a MobileCache can be thought of as dynamic data, kind of like working with data from a dynamic map service or fgdb. The MobileCache is a SQLite database that consists of a schema file containing the map document metadata i.e. what layers there are, how to draw them etc. and a .db file that contains the actual feature and attribute data. There is also a db-journal file created when the MobileCache is open. To use a MobileCache in your application you must first open it from it’s location on disk (this code assumes the data already exists).

private MobileCache OpenCache(string cachePath) 
{ 
    // Set the cache storage location on disk 
    var mobileCache = new MobileCache(cachePath); 
 
    if (!mobileCache.IsValid) return null; 
 
    try 
    { 
        mobileCache.Open(); 
    } 
    catch (Exception ex) 
    { 
        // do something with this 
    } 
 
    return mobileCache.IsEmpty ? null : mobileCache; 
}

If you want to use a traditional cache then you can use the ESRI.ArcGIS.Mobile.DataProducts.RasterData TileCacheMapLayer type. This works from an existing set of tiled images and a tiling scheme file that can be copied onto the device. Be careful when opening data from multiple places in code as you are responsible for opening and closing the data and ensuring it isn’t locked.

public TileCacheMapLayer LoadBaseMap(string path) 
{ 
    var basemap = new TileCacheMapLayer(path); 
    basemap.TileRequestError += (sender, e) => { throw e.TileRequestException; }; 
    basemap.Open(); 
 
    return basemap; 
}

Once you have the data you can add it to your map control as a layer. The draw order is last one added gets drawn first i.e. looks the same order as ArcMap but opposite to Web APIs. Each datasource that is added to the map has to be in the same projection, you can’t mix and match spatial references as there is no built in projection on the fly.

Syncing Data with ArcGIS Server

Whilst it is useful to be able to work with data copied locally onto a device a much more powerful option is being able to sync data between ArcGIS Server and a device. This can be done against a service that has been published with the Mobile Access enabled (note that in order for a service to be enabled for mobile access it must have an explicit extent defined in its underlying map document). The following code shows how to download data filtered by extent and a definition expression, this is to restrict the number of features that are downloaded as we don’t want to get the complete dataset if it is large.

public void Download(string serviceEndpoint, string cachePath, IDictionary<string, IEnumerable<string>> layerQueries) 
{ 
    // Code for downloading features from an ArcGIS service 
    var mobileCache = new MobileCache(cachePath); 
 
    // If the cache already exists then close it. 
    if (mobileCache.CacheExists && mobileCache.IsOpen) 
        mobileCache.Close(); 
 
    try 
    { 
        var serviceConnection = new MobileServiceConnection { Url = serviceEndpoint };
 
        // Create the local cache files on the device and open the connection to the service 
        serviceConnection.CreateCache(mobileCache);
 
        mobileCache.Open(); 
 
        foreach (var layer in layerQueries) 
        { 
            if (layer.Value == null || !layer.Value.Any()) 
                continue;
  
            FeatureLayer featureLayer = mobileCache.FeatureLayers.Single(fl => fl.Name == layer.Key); 
 
            var agent = new FeatureLayerSyncAgent(featureLayer, serviceConnection) 
            { 
                SynchronizationDirection = SyncDirection.DownloadOnly, 
                DownloadFilter = MakeLayerQuery(featureLayer, layer.Value.ToArray()) 
            };
 
            agent.Synchronize(); 
        } 
    } 
    finally 
    { 
        if (mobileCache != null && mobileCache.CacheExists && mobileCache.IsOpen) 
            mobileCache.Close(); 
    } 
} 
 
internal static QueryFilter MakeLayerQuery(FeatureLayer layer, string[] ids) 
{ 
    if (ids == null || !ids.Any()) 
        return new QueryFilter(new Envelope(771809.111674345, 4675666.8666, 2407100.82832565, 6266536.3974), 
                               GeometricRelationshipType.Within); 
 
    if (ids.Count() == 1) 
        return new QueryFilter( 
            new Envelope(771809.111674345, 4675666.8666, 2407100.82832565, 6266536.3974), 
            GeometricRelationshipType.Within, 
            string.Format("{0} = '{1}'", layer.DisplayColumnName, ids.First())); 
 
    // Here we are assuming that the service is configured to have the DisplayColumnName as the correct value 
    return new QueryFilter( 
        new Envelope(771809.111674345, 4675666.8666, 2407100.82832565, 6266536.3974), 
        GeometricRelationshipType.Within, 
        string.Format("{0} in ('{1}')", layer.DisplayColumnName, string.Join("','", ids))); 
}

Looking deeper into the code, the call to serviceConnection.CreateCache(mobileCache); connects to the specified mobile enabled ArcGIS Server service url and downloads the service schema, creating the MapSchema.bin file under the directory location specified. The subsequent call to mobileCache.Open(); creates the MobileCache.db (this is the actual SQLite feature database) and MobileCache.db-journal files. As these file names are the same for each MobileCache it is worth creating each set of data in a different folder location on disk.

If you want to provide some feedback to the user of your application you can hook into the agent.StateChanged and agent.ProgressChanged events. The sequence for a download is

State Ready

State Synchronizing

SyncPhase Downloading, SyncStep – DecomposingExtent

SyncPhase Downloading, SyncStep – ExtentDecomposed, also has the number of features to synchronise

SyncPhase Downloading, SyncStep – DownloadingData

SyncPhase Downloading, SyncStep – DataCached

State Ready

The code required for doing an upload is almost identical, but usually without the need to filter the upload as we want to make sure all our captured data is sent back to the server. The differences are

FeatureLayer featureLayer = mobileCache.FeatureLayers.Single(fl => fl.Name == layerName);
 
if (!featureLayer.HasEdits) 
    continue; 
 
var agent = new FeatureLayerSyncAgent(featureLayer, serviceConnection) 
{ 
    SynchronizationDirection = SyncDirection.UploadOnly 
};
 
agent .Synchronize();

Note that we are only interested in layers that have edits by checking the HasEdits property. If you do want to be able to edit data you must ensure that the underlying feature class has the GlobalID column added to it.

Custom Layers

After you have data on a device another common scenario would be a custom way of visualising it. It’s fairly easy to knock up your own MapActions and Layers so I won’t go into this in detail but a quick tip is that there are some helpers in the SDK for rendering the data so that it looks nice. When you create a custom layer that extends MapGraphicLayer you have to override the Draw method. Rather than using the Graphics to draw your features you can create your own symbol and then use that. For example

public class SampleLayer : MapGraphicLayer 
{ 
    private Font _font; 
    private Symbol _pointSymbol; 
 
    public SampleLayer() : base("Sample layer") 
    { 
        _font = new Font("Tahoma", 24, FontStyle.Bold); 
        _pointSymbol = new Symbol(
            new PointPaintOperation(Color.Red, 1, 0, Color.Red, 0, 6, PointPaintStyle.Circle)); 
    } 
 
    protected override void Dispose(bool disposing) 
    { 
        try 
        { 
            if (disposing) 
            { 
                if (_pointSymbol != null) 
                    _pointSymbol.Dispose(); 
                _pointSymbol = null; 
 
                if (_font != null) 
                    _font.Dispose(); 
                _font = null; 
 
                Location = null; 
            } 
        } 
        finally 
        { 
            base.Dispose(disposing); 
        } 
    } 
 
    private Coordinate _location; 
    public Coordinate Location 
    { 
        get { return _location; } 
        set 
        { 
            _location = value; 
            // notify the map control that you want to redraw this layer 
            OnDataChanged(new DataChangedEventArgs(Map.GetExtent()));                
        } 
    } 
 
    protected override void Draw(Display display) 
    { 
        if (display.DrawingCanceled || !Visible || Map == null || Location == null) 
            return; 
  
        _pointSymbol.DrawPoint(display, Location);
 
        if (display.DrawingCanceled || Location == null) return;
 
        display.DrawText(string.Format("{0:0.000}, {1:0.000}", Location.X, Location.Y), 
            _font, Color.Black, Color.White, Location, TextAlignment.BottomCenter); 
    } 
}

There are a number of different map actions and drawing options to use or extend so I recommend digging into the SDK API reference to find the best match for your needs.


I hope you find this information useful and any feedback is always appreciated.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s