Windows 10, UWP and Experimenting with Inking onto a Map Control (Updated)

Just a quick update to this earlier post;

Windows 10, UWP and Experimenting with Inking onto a Map Control

I got some feedback on that post from a few different places and people pointed out that I could have made more of ‘option 3’ in that post where I’d tried to overlay an InkCanvas on top of a MapControl.

Specifically;

    <Grid
        Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

        <Maps:MapControl 
            x:Name="map"
            MapTapped="OnMapTapped"
            xmlns:Maps="using:Windows.UI.Xaml.Controls.Maps" />

        <InkCanvas
            x:Name="inkCanvas"
            Visibility="Collapsed">
        </InkCanvas>

    </Grid>

Now, I got some feedback on Twitter and elsewhere that I could make that InkCanvas non hit-testable and it’s true that this does work for me (thanks Dave!);

image

but, like Dave, I notice two things if (on OS 10586) I put a non-hit-testable InkCanvas over the top of my MapControl;

  1. I don’t get a MapTapped event as Dave says above.
  2. I notice a reasonable amount of lag when I do a pinch gesture which is going to the map control ‘through’ the InkCanvas control. It feels a little like I’m driving the map control through treacle and it’s not nearly as responsive as it would usually be.

so I veered away from having the InkCanvas initially Visible and using IsHitTestVisible=”False” and moved more towards having the InkCanvas be initially Collapsed as you can see in the XAML above and then dynamically making it visible as/when I wanted it.

That then ties up with this code behind where you’ll notice that I’ve now got 2 different ways of trying to turn an ink stroke into a set of waypoints and I suspect that neither of them are ‘perfect’ by any means and maybe you have a better one;

#define INK_POINTS

namespace Update
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using Windows.Devices.Geolocation;
  using Windows.Foundation;
  using Windows.Services.Maps;
  using Windows.UI;
  using Windows.UI.Input.Inking;
  using Windows.UI.Xaml;
  using Windows.UI.Xaml.Controls;
  using Windows.UI.Xaml.Controls.Maps;

  public sealed partial class MainPage : Page
  {
    public MainPage()
    {
      this.InitializeComponent();

      this.tapPositions = new MapInputEventArgs[POI_COUNT];

      this.Loaded += OnLoaded;
    }
    void OnLoaded(object sender, RoutedEventArgs e)
    {
      this.inkCanvas.InkPresenter.StrokesCollected += OnStrokesCollected;

      var drawingAttr = this.inkCanvas.InkPresenter.CopyDefaultDrawingAttributes();
      drawingAttr.PenTip = PenTipShape.Rectangle;
      drawingAttr.Size = new Size(4, 4);
      drawingAttr.IgnorePressure = true;
      drawingAttr.Color = Colors.Orange;
      this.inkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(drawingAttr);
    }
    Geopoint GeopointFromPoint(Point point)
    {
      Geopoint geoPoint = null;

      this.map.GetLocationFromOffset(point, out geoPoint);

      return (geoPoint);
    }
    async void OnStrokesCollected(InkPresenter sender, InkStrokesCollectedEventArgs args)
    {
      // We're going to take the first stroke, we're not going to try and
      // somehow stitch together many strokes :-S
      var firstStroke = args.Strokes.FirstOrDefault();

      if (firstStroke != null)
      {
        // How to split this down into some waypoints?
        var geoPoints = new List<Geopoint>();

        // add the initial point.
        geoPoints.Add(GeopointFromPoint(this.tapPositions[0].Position));


        // Now try to add some intermediate waypoints to try and 'guide'
        // the route so that it follows the line that's been drawn.

        // I'm not sure how much this next section of code is going to cause
        // duplication by (e.g.) adding the same or similar points along the
        // route due to the ink points potentially being very close together
        // etc.
#if INK_POINTS
        // Add some sprinkling of the ink points...I've chosen 20,40, etc.
        var inkPoints = firstStroke.GetInkPoints();

        const int SAMPLE_RATE = 20;

        for (int i = SAMPLE_RATE; i < inkPoints.Count; i += SAMPLE_RATE)
        {
          geoPoints.Add(GeopointFromPoint(inkPoints[i].Position));
        }
#else
        // Add the positions of the segments that make up the ink stroke.
        foreach (var segment in firstStroke.GetRenderingSegments())
        {
          geoPoints.Add(GeopointFromPoint(segment.Position));
        }
#endif

        // add the follow on point.
        geoPoints.Add(GeopointFromPoint(this.tapPositions[1].Position));

        var routeResult =
          await MapRouteFinder.GetDrivingRouteFromWaypointsAsync(geoPoints);

        // We should do something about failures too
        if (routeResult.Status == MapRouteFinderStatus.Success)
        {
          var mapPolyline = new MapPolyline();
          mapPolyline.Path = routeResult.Route.Path;
          mapPolyline.StrokeThickness = 4;
          mapPolyline.StrokeColor = Colors.Orange;
          mapPolyline.Visible = true;
          this.map.MapElements.Add(mapPolyline);
        }
      }
      this.inkCanvas.Visibility = Visibility.Collapsed;

      this.inkCanvas.InkPresenter.StrokeContainer.Clear();
    }

    void OnMapTapped(MapControl sender, MapInputEventArgs args)
    {
      var mapElementCount = this.map.MapElements.Count;

      if (mapElementCount < POI_COUNT)
      {
        this.tapPositions[mapElementCount] = args;

        var mapIcon = new MapIcon()
        {
          Location = args.Location,
          NormalizedAnchorPoint = new Point(0.5, 0.5)
        };
        this.map.MapElements.Add(mapIcon);

        // When you add a 2nd point of interest, we put an InkCanvas over the entire map
        // waiting to see what you do next. If your next move is to use a pen then 
        // we'll wait for an ink stroke. If your next move is to use something
        // other than a pen then we'll get rid of the InkCanvas and get out of your
        // way.
        mapElementCount = this.map.MapElements.Count;

        if (mapElementCount == POI_COUNT)
        {
          // Switch the ink canvas on and leave it on until the user completes
          // a stroke.
          this.inkCanvas.Visibility = Visibility.Visible;
        }
      }
    }
    static readonly int POI_COUNT = 2;
    MapInputEventArgs[] tapPositions;
  }
}

And that works pretty well for me compared to my original post and it’s quite a bit less code (especially if you removed the conditional compilation).

If I take away the requirement to handle MapTapped event then I could make the InkCanvas initially visible and non hit-testable as in;

    <Grid
        Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

        <Maps:MapControl 
            x:Name="map"
            xmlns:Maps="using:Windows.UI.Xaml.Controls.Maps" />

        <InkCanvas
            x:Name="inkCanvas"
            IsHitTestVisible="False">
        </InkCanvas>

    </Grid>

and then my code behind does a little less as it does not wait for 2 taps to add 2 points of interest but, instead, just waits for a single line to be drawn and uses it to try and sketch out a route;

  using System;
  using System.Collections.Generic;
  using System.Linq;
  using Windows.Devices.Geolocation;
  using Windows.Foundation;
  using Windows.Services.Maps;
  using Windows.UI;
  using Windows.UI.Input.Inking;
  using Windows.UI.Xaml;
  using Windows.UI.Xaml.Controls;
  using Windows.UI.Xaml.Controls.Maps;

  public sealed partial class MainPage : Page
  {
    public MainPage()
    {
      this.InitializeComponent();

      this.Loaded += OnLoaded;
    }
    void OnLoaded(object sender, RoutedEventArgs e)
    {
      this.inkCanvas.InkPresenter.StrokesCollected += OnStrokesCollected;

      var drawingAttr = this.inkCanvas.InkPresenter.CopyDefaultDrawingAttributes();
      drawingAttr.PenTip = PenTipShape.Rectangle;
      drawingAttr.Size = new Size(4, 4);
      drawingAttr.IgnorePressure = true;
      drawingAttr.Color = Colors.Orange;
      this.inkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(drawingAttr);
    }
    Geopoint GeopointFromPoint(Point point)
    {
      Geopoint geoPoint = null;

      this.map.GetLocationFromOffset(point, out geoPoint);

      return (geoPoint);
    }
    async void OnStrokesCollected(InkPresenter sender, InkStrokesCollectedEventArgs args)
    {
      // We're going to take the first stroke, we're not going to try and
      // somehow stitch together many strokes :-S
      var firstStroke = args.Strokes.FirstOrDefault();

      if (firstStroke != null)
      {
        // How to split this down into some waypoints?
        var geoPoints = new List<Geopoint>();

        // Now try to add some intermediate waypoints to try and 'guide'
        // the route so that it follows the line that's been drawn.

        // I'm not sure how much this next section of code is going to cause
        // duplication by (e.g.) adding the same or similar points along the
        // route due to the ink points potentially being very close together
        // etc.
#if INK_POINTS
        // Add some sprinkling of the ink points...I've chosen 20,40, etc.
        var inkPoints = firstStroke.GetInkPoints();

        const int SAMPLE_RATE = 20;

        for (int i = SAMPLE_RATE; i < inkPoints.Count; i += SAMPLE_RATE)
        {
          geoPoints.Add(GeopointFromPoint(inkPoints[i].Position));
        }
#else
        // Add the positions of the segments that make up the ink stroke.
        foreach (var segment in firstStroke.GetRenderingSegments())
        {
          geoPoints.Add(GeopointFromPoint(segment.Position));
        }
#endif

        var routeResult =
          await MapRouteFinder.GetDrivingRouteFromWaypointsAsync(geoPoints);

        // We should do something about failures too
        if (routeResult.Status == MapRouteFinderStatus.Success)
        {
          var mapPolyline = new MapPolyline();
          mapPolyline.Path = routeResult.Route.Path;
          mapPolyline.StrokeThickness = 4;
          mapPolyline.StrokeColor = Colors.Orange;
          mapPolyline.Visible = true;
          this.map.MapElements.Add(mapPolyline);
        }
      }
      this.inkCanvas.InkPresenter.StrokeContainer.Clear();
    }
  }

and that works fine albeit with that slight lag that gets introduced (on OS 10586 at least) by having the InkCanvas over the top of the MapControl.