Runtime Workaround #4: Export a MapView with Overlays

991
0
11-06-2019 11:30 AM
MarkCederholm
Occasional Contributor III
0 0 991

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:  https://community.esri.com/ideas/17558