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