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!


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:  {
   4:  }
   6:  public void SetFeatureSelectedMessage(string message)
   7:  {
   9:  }
  11:  public void SetMapScaleMessage(string message)
  12:  {
  14:  }
  16:  public void SetMapSizeMessage(string message)
  17:  {
  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:  }
   6:  public void SetFeatureSelectedMessage(string message)
   7:  {
   8:      lblMessage.Text = message;
   9:  }
  11:  public void SetMapScaleMessage(string message)
  12:  {
  13:      lblScale.Text = message;
  14:  }
  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;
   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:          }
  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
   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:      }
  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

Thursday, 19 January 2012

mg-desktop has moved

With the implementation of MapGuide RFC117, I have finally migrated the source code for mg-desktop from its current home at Google Code to the official MapGuide Subversion repository

This migration allows for the mg-desktop codebase to better integrate with the MapGuide source that it builds on top of, and allows mg-desktop to receive upstream component fixes and updates much faster and allows for sharing of component dlls with future releases of MapGuide.

Also with everything together in one place, I can finally tackle some of the more interesting things like 64-bit and Linux builds and support for the VS2010 compiler.

The existing Google Code site will remain for archival purposes, but all mg-desktop development will now take place on the official MapGuide repo.

Since we're on the subject of mg-desktop, I might as well show you a visual changelog of the changes and features added to mg-desktop since I first announced it. Most of these changes I am showing in this post are centered on the map viewer component (otherwise there wouldn't be much to show :-)). So without much further ado:

1. Tooltip queries can slow down map interaction, so the default viewer toolbar now includes a command allowing you to toggle display of feature tooltips.

2. The viewer supports customizable selection color

3. To facilitate rapid development, the viewer component works with the Visual Studio designer infrastructure. Viewer properties and behaviour can be modified like you would any other form or control. The OSGeo.MapGuide.Viewer.dll must be registered with the Visual Studio Toolbox to support this workflow.

4. The default viewer toolbar has more useful commands such as:
  • Copying the current view of the map to the clipboard (as an image)
  • Selecting by radius and polygon

5. The legend control now functions like the one in the AJAX or Fusion viewer. Supporting display of themes, and having the ability to apply theme compression (because it too has problems with processing ridiculously large themes)

6. Layer and Group items in the legend control can have context menus attached to them

7. Like the AJAX and Fusion viewers, the property pane supports scrolling through the results of a selection set and zooming into individual results

At this point we have something approaching 90% of the functionality of the AJAX and Fusion viewers. The missing 10% are apparent once you see it:

  • No support for tiled maps. The viewer control has a property that allows tiled maps to be treated as regular groups of dynamic layers as a workaround. The math to calculate what tiles to fetch escapes me right now.
  • No mouse wheel zoom. The math to do this also escapes me right now.
If these items aren't dealbreakers for you, then mg-desktop is a more than suitable platform for building disconnected desktop mapping applications using the same MapGuide and FDO technology that you are all familiar with.

Some useful tools for debugging your MapGuide Applications

A common problem with MapGuide development is trying to dig into the state of the runtime map, especially when trying to figure out why the new layer you've added to your runtime map is not showing. Here's some useful tools to help you peek into the runtime map.

1. mapinfo.aspx

Get the script here (you will also need the supporting viewresourcecontent.aspx script as well)

This was a script I wrote while porting over the MapGuide Developer's Guide samples to .net (which will be bundled with the next MapGuide Open Source release in *all* languages, instead of just PHP) to help me debug a mysterious bug where newly added layers are shown on the map, but not in the viewer legend. The script accepts two URL parameters:

  • MAPNAME - The name of the runtime map
  • SESSION - The current session id
Invoking this script, gives you a nice detailed view about the state of the given runtime map

You'll notice that the resource ids are hyperlinked. These links call into viewresourcecontent.aspx, which basically shows you the resource content of the hyperlinked resource id.

To set this up, drop mapinfo.aspx and viewresourcecontent.aspx into a directory of your choice. Set this directory up as an application in IIS, then create a bin directory and drop in the MapGuide dlls from your mapviewernet directory.

Then to invoke this script, enter http://url-to-your-directory/mapinfo.aspx?MAPNAME=theMapName&SESSION=your-mapguide-session-id

Because this script takes a MAPNAME and a SESSION parameter. It can automatically be used as an Invoke URL command in your Web Layout. Just create an Invoke URL command that points to mapinfo.aspx, attach it to a toolbar or menu and it is ready to be used in your Web Layout for instant debugging!

2. Runtime Map Inspector

Coming in the next release of Maestro, is a Runtime Map Inspector tool which fulfils the same purpose as mapinfo.aspx. Invoking the tool will bring up the familiar Maestro login (which you are advised to use the Administrator login as that can peek into any user's session repository). Once logged in, a new window will appear where you can fill in the required Map Name and Session ID and then click the Load Map button to bring up the runtime map.

The Runtime Map Inspector will be available in the next release under Tools - Runtime Map Inspector, or by running the RtMapInspector.exe

Hopefully, these new tools will make your MapGuide development experience that much more simpler (if it isn't already!)