Friday, 17 February 2012

Scripting and Automating Maestro

Know some Python?

Want to build or customize functionality?

Included with the next release of Maestro, is the IronPython scripting engine, allowing you to script and automate the Maestro application.



A new IronPython Console tab appears the bottom. This is an interactive REPL console that lets you try and experiment with assorted pieces of python code. The whole .net framework libraries (and the Maestro API) are available for use from within the IronPython console.

Unfortunately, there are some .net concepts that do not translate cleanly or at all to Python. One major case is generics. A fair bit of the Maestro Application Services API (the internal API that's built on top of SharpDevelop Core) which drives most of the Maestro application and its addins uses generics. So trying to expose this API in its current form to IronPython would be clunky.

So to rectify this problem, a new Host Application class is being introduced which is accessible via the "app" global variable from the IronPython console. This class provides common functions for interacting with the application itself. For example, this code gets the names of the currently open connections:

connNames = app.GetConnectionNames()

Which we can then use to get the matching connection

conn = app.GetConnection(connNames[0])

"app" is the Host Application object I already mentioned. "conn" is the IServerConnection from the Maestro API. Given that this interface is the root interface of the Maestro API, you can now do any of the things you can do in the Maestro API as you would from a normal .net language.

So fetching a resource is now simply

resXml = app.GetResourceXml(conn, "Library://Samples/Sheboygan/Maps/Sheboygan.MapDefinition")

Or the long way

import clr
clr.AddReference("OSGeo.MapGuide.MaestroAPI")
from OSGeo.MapGuide.MaestroAPI import ResourceTypeRegistry
res = conn.ResourceService.GetResource("Library://Samples/Sheboygan/Maps/Sheboygan.MapDefinition")
resXml = ResourceTypeRegistry.SerializeAsString(res)

You can inspect these variables in two ways:

  1. Reflecting on the variable via the refl command (or reflw to display the result in a new window)
  2. Printing the actual variable value via the print command (or printw to display the result in a new window)
So reflecting the "resXml" variable (reflw resXml) looks like this:



While printing the "resXml" variable (printw resXml) looks like this:




So here's a taste of what you can do with the new scripting capabilities. I'm sure there's some scenarios where scripting would prove to be invaluable for automation and improving productivity.

Many thanks to Joe Socoloski's MIT-licensed IronTextBox for the backbone of this new feature.

Sunday, 12 February 2012

Viewing content changes in Maestro

Ever wondered what the * on that tab actually represents?


With the next release of Maestro, now you can actually find out


The View Changes command lets you see the XML content changes of the currently edited resource


You also have the opportunity to view the XML diff when you try to close an editor tab with unsaved changes



Now those of you with a keen eye may notice some false positives in the first 2 lines.
  • The XML prolog is missing the encoding attribute
  • The root element has its attributes specified in a different order.
It means our XML serialization code is somewhat imperfect, something that I wouldn't have known without a tool like this. Nevertheless, this new tool will let you make a more informed decision when you need to save a modified resource.

Many thanks to Michael Potter for the public domain implementation of the .net diff engine and UI which is the backbone of this new feature.

Friday, 10 February 2012

MapGuide Instant Setup now supports IIS(7)

When I first introduced the MapGuide Instant Setup utility, you could only set up a functional Apache Web Tier.

Well, I finally got motivation to add IIS (7.x) support to this utility, so here's what the IIS configuration screen looks like:



The setup configuration logic is exactly the same as the official installer (ie. It's a series of appcmd.exe calls). The key configuration bits are:

  • The web site indicated by Web Site Name must already exist in IIS
  • The Virtual Directory must not already exist in IIS
  • The Application Pool must not already exist in IIS
  • If the compiled binaries are 64-bit, you must check the This is a 64-bit instance of MapGuide checkbox as this will skip some appcmd.exe calls that apply to 32-bit installs. The Apache configuration does nothing with this field, checked or unchecked.
Will this utility support instant IIS6 or IIS5 configuration in the future? Probably not. Too much messing around with COM objects and metabases for the effort.

Thursday, 9 February 2012

A roadmap update for Maestro

Thought I might give an update about the state of play with the roadmap for MapGuide Maestro

Maestro 5.0


Maestro 5.0 will become the next major release. Here's a brief overview of what you can expect for this release:

  • This will be the first release targeting .net Framework 4.0
  • An addin for working with GeoREST
  • An addin to provide scripting/automation capabilities to Maestro with the IronPython scripting engine.
  • Trickling of assorted functionalities from FDO Toolbox
  • And much more!
The Maestro 5.0 SDK will also hopefully improve your application development experience
  • The Maestro API includes new interfaces for feature source manipulation (Insert, Update, Delete, Apply Schema). The LocalNative connection will have full support for these operations. 
  • The HTTP connection will have partial support for feature source manipulation (currently only Insert) if you include a new GeoREST url connection parameter (there is an implicit assumption here that the mapagent and GeoREST urls you are connecting to are both talking to the same MapGuide Server).
  • LocalNative connections no longer wrap the official API through ugly hacks like assembly binding redirection. LocalNative connection providers will directly reference the official MapGuide .net assemblies.
  • The Maestro SDK will include a new signing utility to automate the previously tedious process of signing the official MapGuide .net assemblies
  • The Maestro SDK will include the source code of the LocalNative connection provider so you can "roll your own" connection provider against your specific version of MapGuide/AIMS

These features will be showcased on this blog as they become implemented and refined. No definite timeline for release, so don't ask :-)


Maestro 4.x maintenance branch


For those of you using the Maestro API, moving to .net Framework 4.0 could be a game changer as the 4.0 CLR may introduce different behaviour to your existing .net code and/or the .net libraries your code uses. The reason for the change is to be able to have LocalNative connection provider support for future releases of MapGuide/AIMS, whose .net assemblies will be targeting .net Framework 4.0.

Maestro, being a .net 2.0 application cannot reference or load assemblies of a newer framework version, so this migration to .net Framework 4.0 is one of necessity. Mono support for .net Framework 4.0 is already quite stable (and we don't venture too far from the well supported subset of WinForms + BCL), so nothing breaking on this front.

So as to not impose an overly breaking change to the existing Maestro API users out there, we will maintain a 4.x branch that will remain targeting .net Framework 2.0 and will only receive bugfixes and minor enhancements. The features I've outlined above will not make it to this branch. Such fixes and minor enhancements will only be made on this maintenance branch only if there is demand for it.

Tuesday, 7 February 2012

mg-desktop: advanced features

Previously, I showed you how to get a basic mg-desktop application up and running.

For this post, I am going to introduce some of the more advanced features and functionality of mg-desktop

Viewer Properties


The mg-desktop viewer (MgMapViewer) exposes a whole bunch of properties that can control the behaviour and appearance of certain features in the map viewer.


Each property is explained below

  • ConvertTiledGroupsToNonTiled - Tiled Layer Groups will be treated as regular map groups, allowing for such groups and layers to be shown in the viewer. This is a simple workaround for the viewer's current inability to display map tiles. 
  • SelectionColor - Controls the color of selected features in the viewer
  • ShowVertexCoordinatesWhenDigitizing - When you are digitizing, the vertex coordinates are shown at each node of the currently digitized geometry. For circles and points, no vertex coordinates are displayed because such shapes have no vertices. For example, digitizing a polygon would look like this:

  • ZoomInFactor - The zoom scale multiplier to apply for zooming in
  • ZoomOutFactor - The zoom scale multiplier to apply for zooming out

Digitizing and Measuring

Like the AJAX and Fusion viewers, the mg-desktop viewer has built-in geometry digitizing functionality.

   1:  public interface IMapViewer
   2:  {
   3:      //
   4:      // Summary:
   5:      //     Starts the digitization process for a circle
   6:      //
   7:      // Parameters:
   8:      //   callback:
   9:      //     The callback to be invoked when the digitization process completes
  10:      void DigitizeCircle(CircleDigitizationCallback callback);
  11:      //
  12:      // Summary:
  13:      //     Starts the digitization process for a line
  14:      //
  15:      // Parameters:
  16:      //   callback:
  17:      //     The callback to be invoked when the digitization process completes
  18:      void DigitizeLine(LineDigitizationCallback callback);
  19:      //
  20:      // Summary:
  21:      //     Starts the digitization process for a line string (polyline)
  22:      //
  23:      // Parameters:
  24:      //   callback:
  25:      //     The callback to be invoked when the digitization process completes
  26:      void DigitizeLineString(LineStringDigitizationCallback callback);
  27:      //
  28:      // Summary:
  29:      //     Starts the digitization process for a point
  30:      //
  31:      // Parameters:
  32:      //   callback:
  33:      //     The callback to be invoked when the digitization process completes
  34:      void DigitizePoint(PointDigitizationCallback callback);
  35:      //
  36:      // Summary:
  37:      //     Starts the digitization process for a polygon
  38:      //
  39:      // Parameters:
  40:      //   callback:
  41:      //     The callback to be invoked when the digitization process completes
  42:      void DigitizePolygon(PolygonDigitizationCallback callback);
  43:      //
  44:      // Summary:
  45:      //     Starts the digitization process for a rectangle
  46:      //
  47:      // Parameters:
  48:      //   callback:
  49:      //     The callback to be invoked when the digitization process completes
  50:      void DigitizeRectangle(RectangleDigitizationCallback callback);
  51:  }

The digitization API is very similar to the AJAX viewer. To digitize a circle for example, you would call the API like so:

   1:  mgMapViewer1.DigitizeCircle(OnCircleDigitized);

The OnCircleDigitized method must match the signature of the CircleDigitizationCallback delegate, which looks like this:

   1:  private void OnCircleDigitized(double x, double y, double radius)
   2:  {
   3:      //x, y is the circle center in the map's coordinates
   4:      //radius is the circle's radius
   5:  }

While digitizing, the digitization process can be aborted by pressing the ESC key. If digitization is aborted, the digitization callback will not be called by the viewer.

Tools like measuring can be built on this functional primitive of digitization. Here's an example using a line digitizer:

   1:  private void btnMeasure_Click(object sender, EventArgs e)
   2:  {
   3:      mgMapViewer1.DigitizeLine(OnLineDigitized);
   4:  }
   5:   
   6:  private void OnLineDigitized(double x1, double y1, double x2, double y2)
   7:  {
   8:      MgMapBase map = mgMapViewer1.GetMap();
   9:      //Create a coordiante system from the map's SRS
  10:      MgCoordinateSystemFactory csFactory = new MgCoordinateSystemFactory();
  11:      MgCoordinateSystem mapCs = csFactory.Create(map.GetMapSRS());
  12:   
  13:      //Invoke the appropriate measure method depending on the type
  14:      //of coordinate system
  15:      double dist = 0.0;
  16:      if (mapCs.GetType() == MgCoordinateSystemType.Geographic)
  17:          dist = mapCs.MeasureGreatCircleDistance(x1, y1, x2, y2);
  18:      else
  19:          dist = mapCs.MeasureEuclideanDistance(x1, y1, x2, y2);
  20:   
  21:      //Convert this distance to meters
  22:      dist = mapCs.ConvertCoordinateSystemUnitsToMeters(dist);
  23:   
  24:      MessageBox.Show("Distance is: " + dist + " meters");
  25:  }

Using the digitizing API combined with the existing MgGeometry and MgCoordinateSystem APIs, you can measure in ways other than simple point A - point B distance measuring.

Redlining


When you combine digitization with feature manipulation provided by the MapGuide API, you have the basis for redlining. Here's an example of creating point features from digitized points:

   1:  private MgdLayer _pointLayer;
   2:   
   3:  private void btnDrawPoint_Click(object sender, EventArgs e)
   4:  {
   5:      mgMapViewer1.DigitizePoint(OnPointDrawn);
   6:  }
   7:   
   8:  private void OnPointDrawn(double x, double y)
   9:  {
  10:      if (_pointLayer == null) //Our point layer doesn't exist
  11:          CreateRedlineLayer();
  12:      
  13:      //Now insert our point. This code should look familiar
  14:      //to you, setting up the MgPropertyCollection for insertion
  15:      MgPropertyCollection props = new MgPropertyCollection();
  16:      MgWktReaderWriter wktRw = new MgWktReaderWriter();
  17:      MgAgfReaderWriter agfRw = new MgAgfReaderWriter();
  18:   
  19:      MgGeometry geom = wktRw.Read("POINT (" + x + " " + y + ")");
  20:      MgByteReader agf = agfRw.Write(geom);
  21:   
  22:      MgGeometryProperty geomProp = new MgGeometryProperty("Geometry", agf);
  23:      props.Add(geomProp);
  24:   
  25:      //Here's where we differ from the official MapGuide API
  26:      //instead of a monolithic UpdateFeatures() that tries to 
  27:      //do everything, we have individual InsertFeatures/DeleteFeatures/UpdateFeatures
  28:      //methods. So here's the mg-desktop way
  29:   
  30:      MgFeatureReader result = _pointLayer.InsertFeatures(props);
  31:      result.Close();
  32:   
  33:      //Or if you have have access to the MgdLayer instance
  34:      /*
  35:      MgResourceIdentifier fsId = new MgResourceIdentifier(_pointLayer.GetFeatureSourceId());
  36:      MgServiceFactory factory = new MgServiceFactory();
  37:      MgdFeatureService featSvc = (MgdFeatureService)factory.CreateService(MgServiceType.FeatureService);
  38:      MgFeatureReader fr = featSvc.InsertFeatures(fsId, "Default:Redline", props);
  39:      fr.Close();
  40:       */
  41:      
  42:      //Now refresh to see your newly drawn point
  43:      mgMapViewer1.RefreshMap();
  44:  }
  45:   
  46:  private void CreateRedlineLayer()
  47:  {
  48:      MgMapBase map = mgMapViewer1.GetMap();
  49:      MgServiceFactory fact = new MgServiceFactory();
  50:      MgdFeatureService featSvc = (MgdFeatureService)fact.CreateService(MgServiceType.FeatureService);
  51:      MgResourceService resSvc = (MgResourceService)fact.CreateService(MgServiceType.ResourceService);
  52:   
  53:      //Note that mg-desktop does not have a concept of sessions like the
  54:      //official MapGuide API, but it *does* allow session-based resources 
  55:      //as a way of having temporary resources. Such resources will reside
  56:      //in a special directory for session resources (specified in Platform.ini)
  57:      //
  58:      //You can plug whatever string as the session id, but the resource identifier
  59:      //must satisfy the session id pattern:
  60:      //
  61:      // Session:<session id string>//Path/To/Your.ResourceType
  62:      //
  63:      //These files are removed with MgPlatform.Terminate(), which is called in this
  64:      //application as part of the exiting process.
  65:      string sessionId = Guid.NewGuid().ToString();
  66:      MgResourceIdentifier fsId = new MgResourceIdentifier("Session:" + sessionId + "//Redline.FeatureSource");
  67:      MgResourceIdentifier ldfId = new MgResourceIdentifier("Session:" + sessionId + "//Redline.LayerDefinition");
  68:   
  69:      //Create our point redline schema. It looks like this:
  70:      //
  71:      // Default
  72:      //    Redline
  73:      //        ID (int32, autogenerated)
  74:      //        Geometry (coordinate system same as map
  75:      string featureClass = "Default:Redline";
  76:      string geometry = "Geometry";
  77:   
  78:      MgFeatureSchema schema = new MgFeatureSchema("Default", "Redline schema");
  79:      MgClassDefinition cls = new MgClassDefinition();
  80:      cls.Name = "Redline";
  81:   
  82:      MgDataPropertyDefinition id = new MgDataPropertyDefinition("ID");
  83:      id.DataType = MgPropertyType.Int32;
  84:      id.SetAutoGeneration(true);
  85:   
  86:      MgGeometricPropertyDefinition geom = new MgGeometricPropertyDefinition(geometry);
  87:      geom.SpatialContextAssociation = "Default";
  88:      geom.GeometryTypes = MgFeatureGeometricType.Curve | MgFeatureGeometricType.Point | MgFeatureGeometricType.Solid | MgFeatureGeometricType.Surface;
  89:   
  90:      MgPropertyDefinitionCollection clsProps = cls.GetProperties();
  91:      clsProps.Add(id);
  92:      clsProps.Add(geom);
  93:   
  94:      MgPropertyDefinitionCollection idProps = cls.GetIdentityProperties();
  95:      idProps.Add(id);
  96:   
  97:      cls.DefaultGeometryPropertyName = geometry;
  98:      MgClassDefinitionCollection classes = schema.GetClasses();
  99:      classes.Add(cls);
 100:   
 101:      //Create the feature source with this schema. We use the map's
 102:      //coordinate system for the feature source to ensure features
 103:      //that we create, will line up with the map
 104:      MgCreateSdfParams create = new MgCreateSdfParams("Default", map.GetMapSRS(), schema);
 105:      featSvc.CreateFeatureSource(fsId, create);
 106:   
 107:      //Then create the layer definition. RedlineLayer.xml contains the template
 108:      string xml = string.Format(File.ReadAllText("RedlineLayer.xml"), fsId.ToString(), featureClass, geometry);
 109:      var bytes = Encoding.UTF8.GetBytes(xml);
 110:      MgByteSource source = new MgByteSource(bytes, bytes.Length);
 111:      resSvc.SetResource(ldfId, source.GetReader(), null);
 112:   
 113:      //Now create the runtime layer and add to map
 114:      _pointLayer = new MgdLayer(ldfId, resSvc);
 115:      _pointLayer.LegendLabel = "Redlining";
 116:      _pointLayer.Name = "Redline";
 117:      _pointLayer.Visible = true;
 118:      _pointLayer.Selectable = true;
 119:      _pointLayer.DisplayInLegend = true;
 120:   
 121:      var layers = map.GetLayers();
 122:      layers.Insert(0, _pointLayer);
 123:  }

An updated sample (building on the previous sample application) containing the above snippets is available for download here

Tuesday, 24 January 2012

Updated OSGeo Nabble archive link

Those of you who prefer a forum-based frontend to the mapguide-users and other OSGeo mailing lists probably had warning about the migration of the OSGeo mailing list archives by nabble.

Well, in addition to breaking every existing mailing list link (I bet you all the nabble posts I've linked from this blog are completely broken now!), the old archive no longer gives you the link to the new archive location. Real smart move by Nabble!

Well in case anyone is wondering, here's the new link before nabble took it down along with the old archive: http://osgeo-org.1560.n6.nabble.com/

What's even funny is the notice at the top of the new archive location

We have moved the OSGeo.org archives to this location. Please update your bookmarks.

And just who is going to be able to read that? Certainly not the people who have been inconvenienced by this ill-thought out move!

/rant

Monday, 23 January 2012

How to: Use mg-desktop in your own .net applications

Here I am, talking about mg-desktop and how it's the next best thing since sliced bread, but I haven't even explained how you can go about using it in your own .net applications! This post aims to rectify this problem.

This post will aim to show you how to create a simple .net WinForms application that displays a map from the Sheboygan sample data set with a basic selection handler. The final application will look like so:



Before we get started, make sure you have the following:
  • Visual Studio 2008 or newer (I'm using the express edition for this post)
  • The latest binary release of mg-desktop
Knowledge of the official MapGuide .net API is also assumed here because most of what you know about the official API is equally applicable to mg-desktop.

So fire up Visual Studio and let's get started!

1. Download mg-desktop

Download the latest binary release of mg-desktop and extract this archive to a directory of your choice. We will be referring to files in this location for the rest of this post.

2. Set up the Visual Studio Toolbox

In order to facilitate drag-and-drop of the map viewer component, we need to register the mg-desktop viewer component into the Visual Studio Toolbox. To do this, right click the toolbox and select Choose Items



This will bring up the Choose Toolbox Items dialog, click the browse button


Browse to the directory you extracted mg-desktop into and select the OSGeo.MapGuide.Viewer.dll assembly. This will add our viewer components to the list of available components.

The components for reference, are:
  • MgMapViewer - This is the map viewer component
  • MgLegend - This is the legend component which can control the display and visibility of layers in the map viewer. This component is optional
  • MgPropertyPane - This is the component for displaying attributes of selected features on the map viewer. This component is optional
  • MgDefaultToolbar - This is a component containing a common list of functions for interacting with the map viewer (zoom, pan, select, etc). This component is optional. You can roll your own map viewer toolbar, but that requires a lot of boilerplate to set up. This component is provided for convenience.


Ensure these components are ticked and click OK to add these components to the Visual Studio Toolbox. 

3. Create a new WinForms project

NOTE: The mg-desktop map viewer is a WinForms component. You can technically use this component in a WPF application using the WPF-WinForms interop libraries, but that is beyond the scope of this tutorial.

Now we create our WinForms application. Select File - New Project and select the Windows Forms Application template.


4. Build our main form

If you look at the Visual Studio Toolbox, your components should now be visible whenever the WinForms designer is active



Drag and drop the MgDefaultToolbar component into the main form



Now drag and drop a StatusStrip component into the main form. 



Add 4 labels to this status strip. These labels will be used to show the following:
  1. The current mouse coordinates
  2. Any status messages sent by the viewer
  3. The current scale
  4. The size of the map



Now add a SplitContainer to the main part of the form




Add a second SplitContainer to the left side of this form with horizontal orientation




Now we can drag and drop the remaining components. Set all components to Dock = Fill to occupy the full space of its container
  • Drag and Drop the MgLegend to the top-left panel
  • Drag and Drop the MgPropertyPane to the bottom-left panel
  • Finally, drag and drop the MgMapViewer to the main panel



Modify the properties of the lblMessage label as such:
  • Spring = true
  • TextAlign = MiddleLeft
This will ensure this label takes the maximum space in the status bar




Now we need to write some code.

5. Wire-up the viewer components

Now switch to the code view for Form1. Start by adding importing the OSGeo.MapGuide.Viewer namespace

   1:  using OSGeo.MapGuide.Viewer;

To show map viewer status messages, we need this form to implement the IMapStatusBar interface. This adds the following methods to our form

   1:  public void SetCursorPositionMessage(string message)
   2:  {
   3:      
   4:  }
   5:   
   6:  public void SetFeatureSelectedMessage(string message)
   7:  {
   8:      
   9:  }
  10:   
  11:  public void SetMapScaleMessage(string message)
  12:  {
  13:      
  14:  }
  15:   
  16:  public void SetMapSizeMessage(string message)
  17:  {
  18:      
  19:  }

These methods should be self explanatory. Simply connect the message parameter to its respective label

   1:  public void SetCursorPositionMessage(string message)
   2:  {
   3:      lblCoordinates.Text = message;
   4:  }
   5:   
   6:  public void SetFeatureSelectedMessage(string message)
   7:  {
   8:      lblMessage.Text = message;
   9:  }
  10:   
  11:  public void SetMapScaleMessage(string message)
  12:  {
  13:      lblScale.Text = message;
  14:  }
  15:   
  16:  public void SetMapSizeMessage(string message)
  17:  {
  18:      lblSize.Text = message;
  19:  }

Now how do we tie all of these components (viewer, toolbar, legend, property pane) together? We use a MapViewerController to do this. Override the OnLoad method like so:

   1:  protected override void OnLoad(EventArgs e)
   2:  {
   3:      new MapViewerController(mgMapViewer1,          //The MgMapViewer
   4:                              mgLegend1,             //The MgLegend
   5:                              this,                  //The IMapStatusBar
   6:                              mgPropertyPane1,       //The MgPropertyPane
   7:                              mgDefaultToolbar1);    //The MgDefaultToolbar
   8:  }

That one line (5 if you want to be pedantic), magically ties all our viewer components together. The MapViewerController basically handles all the plumbing so that your viewer components will properly communicate with each other. Some examples, include:
  • Selecting an object in the MgMapViewer will populate the MgPropertyPane with attributes of the selected feature
  • Ticking a layer on/off in the MgLegend will trigger a refresh of the MgMapViewer
The MapViewerController automagically sets up all of this for you.

Now we have a viewer that's all set up, now to load some data into it. 

6. Code - Initialization

Before we show you how to do this, let's take a segway for a moment. Because we need to cover an important aspect of the mg-desktop API. 

Like the official API, the mg-desktop API is driven by service classes. In mg-desktop, the following services are provided:
  • MgdResourceService (inherits from MgResourceService)
  • MgdFeatureService (inherits from MgFeatureService)
  • MgRenderingService
  • MgDrawingService
  • MgTileService
In the official API, you would access these services via a MgSiteConnection object. For mg-desktop we use the MgServiceFactory class to create instances of these services. For example, here's how you would create an instance of MgdFeatureService

   1:  MgServiceFactory factory = new MgServiceFactory();
   2:  MgdFeatureService featureService = (MgdFeatureService)factory.CreateService(MgServiceType.FeatureService);

Other service classes are created in a similar fashion. You would then use these service classes in the same fashion as you would with the official API.

Also like the official API, we need to initialize the whole thing through a config file first before we can use any of the classes in the API. In our case, the file is Platform.ini, and we initialize like so:

   1:  MgPlatform.Initialize("Platform.ini");

With that out of the way, we can start writing some code. First we need to add some references to the project. Add the following references from your mg-desktop directory
  • OSGeo.MapGuide.Foundation.dll
  • OSGeo.MapGuide.Geometry.dll
  • OSGeo.MapGuide.PlatformBase.dll
  • OSGeo.MapGuide.Desktop.dll
  • OSGeo.MapGuide.Viewer.Desktop.dll
Be sure to set these references (and OSGeo.MapGuide.Viewer) to (Copy Local = false)

Now in our application's entry point, insert our call to initialize the API. Also hook the application's exit event to MgPlatform.Terminate(), which does some library cleanup. Program.cs should look like this

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Windows.Forms;
   5:  using OSGeo.MapGuide;
   6:   
   7:  namespace MgDesktopSample
   8:  {
   9:      static class Program
  10:      {
  11:          /// <summary>
  12:          /// The main entry point for the application.
  13:          /// </summary>
  14:          [STAThread]
  15:          static void Main()
  16:          {
  17:              MgPlatform.Initialize("Platform.ini");
  18:              Application.ApplicationExit += new EventHandler(OnApplicationExit);
  19:              Application.EnableVisualStyles();
  20:              Application.SetCompatibleTextRenderingDefault(false);
  21:              Application.Run(new Form1());
  22:          }
  23:   
  24:          static void OnApplicationExit(object sender, EventArgs e)
  25:          {
  26:              MgPlatform.Terminate();
  27:          }
  28:      }
  29:  }


7. Code - Load a package and map


Now back in our main form, we modify the overridden OnLoad to do the following:
  • Check the existence of the map we want to load -  Library://Samples/Sheboygan/Maps/Sheboygan.MapDefinition
  • If it doesn't exist, prompt the user for the Sheboygan.mgp package and load this package
   1:  MgServiceFactory factory = new MgServiceFactory();
   2:  MgdResourceService resSvc = (MgdResourceService)factory.CreateService(MgServiceType.ResourceService);
   3:  MgResourceIdentifier mapDefId = new MgResourceIdentifier("Library://Samples/Sheboygan/Maps/Sheboygan.MapDefinition");
   4:  //If this map definition doesn't exist, we ask the user to
   5:  //load the Sheboygan package
   6:  if (!resSvc.ResourceExists(mapDefId))
   7:  {
   8:      using (OpenFileDialog diag = new OpenFileDialog())
   9:      {
  10:          diag.Filter = "MapGuide Packages (*.mgp)|*.mgp";
  11:          if (diag.ShowDialog() == DialogResult.OK)
  12:          {
  13:              MgByteSource source = new MgByteSource(diag.FileName);
  14:              MgByteReader reader = source.GetReader();
  15:              resSvc.ApplyResourcePackage(reader);
  16:          }
  17:          else
  18:          {
  19:              //No map, nothing to do here
  20:              Application.Exit();
  21:          }
  22:      }
  23:  }

At this point, the map definition exists. So we can create a runtime map and load it into the viewer like so:

   1:  //Create our runtime map
   2:  MgdMap map = new MgdMap(mapDefId);
   3:  //We need a rendering service instance
   4:  MgRenderingService renderSvc = (MgRenderingService)factory.CreateService(MgServiceType.RenderingService);
   5:  //Create our viewer provider
   6:  MgMapViewerProvider provider = new MgDesktopMapViewerProvider(map, resSvc, renderSvc);
   7:  //Initialize our viewer with this provider
   8:  mgMapViewer1.Init(provider);

The final OnLoad method for our form looks like so:

   1:  protected override void OnLoad(EventArgs e)
   2:  {
   3:      new MapViewerController(mgMapViewer1,          //The MgMapViewer
   4:                              mgLegend1,             //The MgLegend
   5:                              this,                  //The IMapStatusBar
   6:                              mgPropertyPane1,       //The MgPropertyPane
   7:                              mgDefaultToolbar1);    //The MgDefaultToolbar
   8:   
   9:      MgServiceFactory factory = new MgServiceFactory();
  10:      MgdResourceService resSvc = (MgdResourceService)factory.CreateService(MgServiceType.ResourceService);
  11:      MgResourceIdentifier mapDefId = new MgResourceIdentifier("Library://Samples/Sheboygan/Maps/Sheboygan.MapDefinition");
  12:      //If this map definition doesn't exist, we ask the user to
  13:      //load the Sheboygan package
  14:      if (!resSvc.ResourceExists(mapDefId))
  15:      {
  16:          using (OpenFileDialog diag = new OpenFileDialog())
  17:          {
  18:              diag.Filter = "MapGuide Packages (*.mgp)|*.mgp";
  19:              if (diag.ShowDialog() == DialogResult.OK)
  20:              {
  21:                  MgByteSource source = new MgByteSource(diag.FileName);
  22:                  MgByteReader reader = source.GetReader();
  23:                  resSvc.ApplyResourcePackage(reader);
  24:              }
  25:              else
  26:              {
  27:                  //No map, nothing to do here
  28:                  Application.Exit();
  29:              }
  30:          }
  31:      }
  32:   
  33:      //Create our runtime map
  34:      MgdMap map = new MgdMap(mapDefId);
  35:      //We need a rendering service instance
  36:      MgRenderingService renderSvc = (MgRenderingService)factory.CreateService(MgServiceType.RenderingService);
  37:      //Create our viewer provider
  38:      MgMapViewerProvider provider = new MgDesktopMapViewerProvider(map, resSvc, renderSvc);
  39:      //Initialize our viewer with this provider
  40:      mgMapViewer1.Init(provider);
  41:  }

7. Set up post-build and other loose ends


Now if you've worked with the official MapGuide .net API (and this whole post assumes you do), you know that referencing the MapGuide .net assemblies does not instantly give you a working MapGuide application. That's because those .net assemblies are managed wrappers around unmanaged dlls, so you need them as well. So for the official API, you would copy all the dlls from mapviewernet into your application's output directory so that all dependencies are met.

For mg-desktop, we pretty much do the same thing, we copy everything from our mg-desktop directory to our application's output directory. Or to automate this, include an xcopy command as part of your project's post build event. Assuming you extracted the mg-desktop binaries to C:\mg-desktop, and example post build command would be like so:


This will copy all mg-desktop files (dlls, FDO, CS-Map dictionaries, etc, etc) to your application's output directory with the source directory structure intact, which is important because the default paths in Platform.ini are all relative.

If you are on a 64-bit machine, you will also need to explicitly set the CPU type of the application to x86 instead of Any CPU. If you don't do this, you will get a BadImageFormatException thrown at your face as your executable will default to 64-bit and will attempt to load a 32-bit assembly. Actually, you should do this anyway to ensure the application works on both 32-bit and 64-bit windows.

Once this is all set up, you can compile and run your application!


Go on. Have a play around. It is now a fully functional map viewer application!

8. Custom selection handling


One of the things you would probably want to do in your application is to listen for selection changes and run code in response to such changes. The MgMapViewer component exposes a SelectionChanged event for this very purpose.

So to display the address of a selected parcel, the event handler code would look like this:

   1:  private void mgMapViewer1_SelectionChanged(object sender, EventArgs e)
   2:  {
   3:      MgSelectionBase selection = mgMapViewer1.GetSelection();
   4:      MgReadOnlyLayerCollection layers = selection.GetLayers();
   5:      if (layers != null)
   6:      {
   7:          for (int i = 0; i < layers.GetCount(); i++)
   8:          {
   9:              MgLayerBase layer = layers.GetItem(i);
  10:              if (layer.Name == "Parcels") //The selected layer is parcels
  11:              {
  12:                  //Check that we only have one selected object
  13:                  int count = selection.GetSelectedFeaturesCount(layer, layer.FeatureClassName);
  14:                  if (count == 1)
  15:                  {
  16:                      MgFeatureReader reader = null;
  17:                      try
  18:                      {
  19:                          reader = selection.GetSelectedFeatures(layer, layer.FeatureClassName, false);
  20:                          if (reader.ReadNext())
  21:                          {
  22:                              //Address is in the RPROPAD property
  23:                              if (reader.IsNull("RPROPAD"))
  24:                                  MessageBox.Show("Selected parcel has no address");
  25:                              else
  26:                                  MessageBox.Show("Address: " + reader.GetString("RPROPAD"));
  27:                          }
  28:                      }
  29:                      finally //You must always close all readers, otherwise connections will leak
  30:                      {
  31:                          reader.Close();
  32:                      }
  33:                  }
  34:                  else
  35:                  {
  36:                      MessageBox.Show("Please select only one parcel");
  37:                  }
  38:                  break;
  39:              }
  40:          }
  41:      }
  42:  }

Which would result in this behaviour when selecting a parcel


Selecting multiple parcels gives you the following:


Wrapping up


Hopefully this should give you a comfortable introduction to mg-desktop and its viewer component. Where you go from here is completely up to you.

The source code for this example is available for download here