MCederholm

Runtime Workaround #4: Export a MapView with Overlays

Blog Post created by MCederholm on Nov 6, 2019

At some point in the 100.x lifespan of ArcGIS Runtime SDK for .NET, the old tried-and-true method of treating a MapView as just another WPF Visual went sailing out the window.  Granted, the ExportImageAsync method should have been a simple workaround, but for one drawback: overlay items are not included!

 

Now I don't know about you, but I find the OverlayItemsControl to be a great way to add interactive text to a map.  You can have it respond to a mouse-over:

 

 

Bring up a context menu:

 

 

Modify properties:

 

 

And so on.  In the old days, when you created an image of the MapView, the overlays would just come right along:

 

          private RenderTargetBitmap GetMapImage(MapView mv)
          {

               // Save map transform

               System.Windows.Media.Transform t = mv.LayoutTransform;
               Rect r = System.Windows.Controls.Primitives.LayoutInformation.GetLayoutSlot(mv);
               mv.LayoutTransform = null;
               Size sz = new Size(mv.ActualWidth, mv.ActualHeight);
               mv.Measure(sz);
               mv.Arrange(new Rect(sz));

               // Output map

               RenderTargetBitmap rtBitmap = new RenderTargetBitmap(
                    (int)sz.Width, (int)sz.Height, 96d, 96d,
                    System.Windows.Media.PixelFormats.Pbgra32);
               rtBitmap.Render(mv);

               // Restore map transform

               mv.Arrange(r);
               mv.LayoutTransform = t;

               return rtBitmap;

          }

 

Not so today!  Try that approach in 100.6 and you just get a black box.    

 

My workaround:

 

  1. Create a Canvas
  2. Create an Image for the Mapview and add it to the Canvas
  3. Create an Image for every overlay and add it to the Canvas
  4. Create a bitmap from the Canvas

 

Step 3 is trickier than you would think, however, because of two issues:  1) relating the anchor point to the overlay, and 2) taking any RenderTransform into account.

 

As far as I can tell, this is the rule for determining the relationship between the overlay and the anchor point:

 

HorizontalAlignment: Center or Stretch, anchor point is at the center; Left, anchor point is at the right; Right, anchor point is at the left.

VerticalAlignment: Center or Stretch, anchor point is at the center; Top, anchor point is at the bottom; Bottom, anchor point is at the top.

For a Canvas element, the anchor point is at 0,0 -- however, I have not found a good way to create an Image from a Canvas [if the actual width and height are unknown].

 

To create an Image from the element, any RenderTransform must be removed before generating the RenderTargetBitmap.  Then, the Transform must be reapplied to the Image.  Also, you need to preserve HorizontalAlignment and VerticalAlignment if you're creating a page layout using a copy of the MapView, so that the anchor point placement is correct.

 

So here it is, the code for my workaround:

 

using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;

using Esri.ArcGISRuntime.Geometry;
using Esri.ArcGISRuntime.UI;
using Esri.ArcGISRuntime.UI.Controls;

namespace Workarounds
{

     public struct MapOverlayExport
     {
          public Image OverlayImage;
          public MapPoint Anchor;
          public MapPoint TopLeft;
     }

     public static class MapExportHelper
     {

          // Export bitmap from map with XAML graphics overlays

          public static async Task<ImageSource> GetMapImage(MapView mv)
          {

               RuntimeImage ri = await mv.ExportImageAsync();
               ImageSource src = await ri.ToImageSourceAsync();
               if (mv.Overlays.Items.Count == 0)
                    return src; // No XAML overlays

               // Create canvas

               double dWidth = mv.ActualWidth;
               double dHeight = mv.ActualHeight;
               Rect rMap = new Rect(0, 0, dWidth, dHeight);
               Size szMap = new Size(dWidth, dHeight);
               Canvas c = new Canvas();

               // Add map image

               Image imgMap = new Image()
               {
                    Height = dHeight,
                    Width = dWidth,
                    Source = src
               };
               imgMap.Measure(szMap);
               imgMap.Arrange(rMap);
               imgMap.UpdateLayout();
               Canvas.SetTop(imgMap, 0);
               Canvas.SetLeft(imgMap, 0);
               c.Children.Add(imgMap);

               // Add map overlays

               List<MapOverlayExport> Overlays = GetMapOverlays(mv);
               foreach (MapOverlayExport overlay in Overlays)
               {

                    // Get Image and location

                    Image img = overlay.OverlayImage;
                    MapPoint ptMap = overlay.TopLeft;
                    Point ptScreen = mv.LocationToScreen(ptMap);

                    // Create and place image of element

                    Canvas.SetTop(img, ptScreen.Y);
                    Canvas.SetLeft(img, ptScreen.X);
                    c.Children.Add(img);
                    img.UpdateLayout();

               }
               c.Measure(szMap);
               c.Arrange(rMap);
               c.UpdateLayout();

               // Create RenderTargetBitmap

               RenderTargetBitmap rtBitmap = new RenderTargetBitmap(
                    (int)dWidth, (int)dHeight, 96d, 96d, PixelFormats.Pbgra32);
               rtBitmap.Render(c);
               return rtBitmap;

          }

          public static List<MapOverlayExport> GetMapOverlays(MapView mv)
          {

               List<MapOverlayExport> Overlays = new List<MapOverlayExport>();
               foreach (object obj in mv.Overlays.Items)
               {

                    // Get element and location

                    if (!(obj is FrameworkElement elem))
                    {
                         Debug.Print("MapExportHelper: Non-FrameworkElement encountered.");
                         continue;
                    }
                    double dW = elem.ActualWidth;
                    double dH = elem.ActualHeight;
                    if ((dH == 0) || (dW == 0))
                    {
                         Debug.Print("MapExportHelper: Unsupported FrameworkElement encountered.");
                         continue;
                    }

                    // Remove RenderTransform and RenderTransformOrigin

                    Transform tRender = elem.RenderTransform;
                    Point ptOrigin = elem.RenderTransformOrigin;
                    elem.RenderTransform = null;
                    elem.RenderTransformOrigin = new Point(0,0);
                    elem.Measure(new Size(dW, dH));
                    elem.Arrange(new Rect(0, 0, dW, dH));
                    elem.UpdateLayout();

                    // Create image of element

                    ImageSource src = null;
                    if (elem is Image imgSrc)
                         src = imgSrc.Source;
                    else
                    {
                         RenderTargetBitmap bmp = new RenderTargetBitmap(
                              (int)dW, (int)dH, 96d, 96d, PixelFormats.Pbgra32);
                         bmp.Render(elem);
                         src = bmp;
                    }
                    Image img = new Image()
                    {
                         Height = dH,
                         Width = dW,
                         Source = src,
                         HorizontalAlignment = elem.HorizontalAlignment,
                         VerticalAlignment = elem.VerticalAlignment,
                         RenderTransform = tRender,
                         RenderTransformOrigin = ptOrigin
                    };

                    // Restore RenderTransform and RenderTransformOrigin

                    elem.RenderTransform = tRender;
                    elem.RenderTransformOrigin = ptOrigin;

                    // Find top left location in map coordinates

                    MapPoint ptMap = MapView.GetViewOverlayAnchor(elem);
                    Point ptScreen = mv.LocationToScreen(ptMap);
                    double dY = 0;
                    double dX = 0;
                    switch (elem.VerticalAlignment)
                    {
                         case VerticalAlignment.Center:
                         case VerticalAlignment.Stretch:
                              dY = -dH / 2;
                              break;
                         case VerticalAlignment.Top:
                              dY = -dH;
                              break;
                    }
                    switch (elem.HorizontalAlignment)
                    {
                         case HorizontalAlignment.Center:
                         case HorizontalAlignment.Stretch:
                              dX = -dW / 2;
                              break;
                         case HorizontalAlignment.Left:
                              dX = -dW;
                              break;
                    }
                    Point ptTopLeftScreen = new Point(ptScreen.X + dX, ptScreen.Y + dY);
                    MapPoint ptTopLeftMap = mv.ScreenToLocation(ptTopLeftScreen);

                    // Add exported overlay to list

                    Overlays.Add(new MapOverlayExport()
                    {
                         OverlayImage = img,
                         Anchor = ptMap,
                         TopLeft = ptTopLeftMap
                    });

               }

               return Overlays;

          }

     }
}

 

P.S. -- If you want ExportImageAsync to include overlays, vote up this idea:  GeoView.ExportImageAsync should include overlays 

Outcomes