This project has moved. For the latest updates, please go here.

Draggable Route Handles

Mar 22 at 2:44 PM
Edited Mar 22 at 6:30 PM
Hi! After weighing several map controls, we decided on XAMLMapControl for our application, and for about a year now it has performed very well. However, two issues exist with our usage of it. I would very much appreciate input, whether it be identifying the cause of issues in our solution or suggested alternative means of accomplishing our requirements.

Since the situation is somewhat complicated, I've divided up this post into a description of the use-case (Requirements) and the problems with our current solution (see Problem 1 and Problem 2). Because of the per-post content limit, see the following posts for the relevant code snippets.

Requirements

I am using this control for a route-planning system. In my application, a Route is divided into one or more sub-routes called Runs, each of which has a list of points.

My requirements are that the route is displayed and provides a context menu with commands (such as adding a vehicle to follow the route or removing the route entirely). When the user hovers over the route, "handles" must appear that allow dragging the route's points into new positions.

(As well, the user must be able to type refined coordinates in to edit the route, but I address this outside of the map control -- this requirement's only impact is that point updates must be bidirectional, but INotifyPropertyChanged takes care of that anyway).

In addition, my application requires that the runs are labeled, and as a bonus I am marking the first point of the first run with a green circle (Start) and the last point of the last run with a checkerboard circle (End). Animating the lines allows the direction to be easily observed when there are lots of routes on the map.

Problem

This all works except for two problems.

Problem 1: Handle Movement

Right now, when I drag a handle, the route line itself moves but the handle stays where it was until the user has stopped dragging. This is the primary issue with Problem 1.

Futhermore, if there are several runs and the user drags the point that separates them, one handle will float at its previous coordinates until the map is "jiggled." (Note that the endpoints of runs overlap and a ReactiveUI subscription propagates modifications from the end of one to the start of the other, and vice-versa). This might have a different cause.

Note
The start and end points update properly, so it might have to do with the handles being in a list and drawn with a MapItemsControl.

Problem 2: Route Selection / Context Menu

The map keeps track of which object is selected (which may or may not be a route -- there's lots of other objects on the screen as well). If the user moves the mouse to the first vertex of a route, such that the handles appear and the user is over the first one, and then the user right-clicks, the right-click does not trigger selection of the route in the viewmodel. Without that selection being set, the route's context menu commands (such as "Remove Route") cannot execute.

I have a MapView and a MapViewModel. Several different MapItemsControl objects exist on the map, and they all bind to the same property SelectedMapItem in MapViewModel. If a PolylineRun is selected, it will find the containing Route and select that instead.

Note
This problem goes away when I remove the handles. I suspect that the selection of the handles' MapItemsControl is blocking the selection of the route as a whole. However, when I try hooking up a property to listen for individual point selection and bounce it back up to MapViewModel, it still doesn't work in all cases.

Code

Specific code samples follow; here is an overview.

Much of this code is based on the solution from your earlier thread:

https://xamlmapcontrol.codeplex.com/discussions/636823

For the purpose of integration with other code (and to support ReactiveUI), I have modified the control to use my own version of the Location class, called LlaPosition, that inherits ReactiveObject (which, used properly, takes care of INotifyPropertyChanged as well as provides other tools).

The Route class has a ReactiveList (the ReactiveUI equivalent of ObservableCollection) called Runs that holds objects of type PolylineRun. The PolyLineRun class, in turn, has a ReactiveList called Locations that holds objects of type LlaPosition.

To encapsulate this structure, there is a custom UserControl called DraggableMapPolyline. Each Run gets its own DraggableMapPolyline.

The main MapView has a MapItemsControl for the Routes. This MapItemsControl has its its ItemContainerStyle set to MapItem, which in turn is styled with a ControlTemplate that hosts yet another MapItemsControl.

The inner MapItemsControl is for the Runs, and it's ControlTemplate ends up as a DraggableMapPolyline (which in turn hosts a MapItemsControl for the handles).
Mar 23 at 4:14 PM
Edited Mar 23 at 5:43 PM
Hi again,

I've stripped it down and the problem persists.

In summary: I have a draggable polyline class with "handles" that allow dragging individual points to new locations. A "route" is broken into subroutes, each one of which gets this draggable poyline.

There are two problems:
  1. Dragging a handle moves the line but doesn't move the handle point itself (though the handle does move to the right location when the map is panned/zoomed).
  2. Right-clicking a handle doesn't notify the viewmodel that the subroute has been selected, which breaks my context menus.
Image

I tried to borrow from the Map Ruler you proposed in another thread, but it gets more complicated since I need to use any number of points. This led to a hacky solution in MoveHandle().

Everything in here is based off of ReactiveUI, which we went with because ReactiveLists can tell you when an object in them is modified at all, not just when an element is replaced.

The TestRoute and TestSubroute classes I thought were out of scope, but I can post them on demand. All they are is that TestRoute has a ReactiveList<TestSubroute> which has a ReactiveList<LlaPosition> called Locations.

MapView
<mapControl:MapItemsControl ItemsSource="{Binding TestRoutes}">
    <mapControl:MapItemsControl.ItemContainerStyle>
        <Style TargetType="mapControl:MapItem">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate>
                        <mapControl:MapItemsControl ItemsSource="{Binding TestSubroutes}"
                                                    SelectedValue="{Binding Path=Data.SelectedMapItem, Source={StaticResource Proxy}}"
                                                    IsSynchronizedWithCurrentItem="True">
                            <mapControl:MapItemsControl.ItemContainerStyle>
                                <Style TargetType="mapControl:MapItem">
                                    <Setter Property="Template">
                                        <Setter.Value>
                                            <ControlTemplate>
                                                <controls:TestDraggableMapPolyline Locations="{Binding Locations}"/>
                                            </ControlTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </Style>
                            </mapControl:MapItemsControl.ItemContainerStyle>
                        </mapControl:MapItemsControl>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="ContextMenu">
                <Setter.Value>
                    <ContextMenu>
                        <MenuItem Header="Remove Route"
                                  Command="{Binding Data.RemoveRouteCommand, Source={StaticResource Proxy}}" />
                    </ContextMenu>
                </Setter.Value>
            </Setter>
        </Style>
    </mapControl:MapItemsControl.ItemContainerStyle>
</mapControl:MapItemsControl>
TestDraggableMapPolyline.xaml
<UserControl x:Class="RouteDraggingProjectTest.Presentation.Controls.TestDraggableMapPolyline"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:RouteDraggingProjectTest.Presentation.Controls"
             xmlns:mapControl="clr-namespace:MapControl;assembly=MapControl.WPF"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    
    <mapControl:MapPanel x:Name="Panel">
        
        <mapControl:MapPolyline
            Locations="{Binding Locations, RelativeSource={RelativeSource AncestorType=UserControl}}"
            IsClosed="False"
            MouseEnter="ShowHandles"
            MouseLeave="HideHandles"
            Stroke="Blue"
            StrokeThickness="5"/>
        
        <mapControl:MapItemsControl x:Name="Handles"
                                    ItemsSource="{Binding Locations, RelativeSource={RelativeSource AncestorType=UserControl}}">
         
            <mapControl:MapItemsControl.ItemContainerStyle>
                <Style TargetType="mapControl:MapItem">
                    <Setter Property="mapControl:MapPanel.Location" Value="{Binding}" />
                </Style>
            </mapControl:MapItemsControl.ItemContainerStyle>
            
            <mapControl:MapItemsControl.ItemTemplate>
                <DataTemplate>
                    <Path Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=UserControl}}"
                          MouseLeftButtonDown="HandleLeftButtonDown"
                          MouseLeftButtonUp="HandleLeftButtonUp"
                          MouseMove="MoveHandle"
                          MouseEnter="ShowHandles"
                          MouseLeave="HideHandles">
                        <Path.Data>
                            <EllipseGeometry RadiusX="15" RadiusY="15" />
                        </Path.Data>
                    </Path>
                </DataTemplate>
            </mapControl:MapItemsControl.ItemTemplate>
            
        </mapControl:MapItemsControl>
        
    </mapControl:MapPanel>
    
</UserControl>
TestDraggableMapPolyline.xaml.cs
 public partial class TestDraggableMapPolyline : UserControl
{
    public static readonly DependencyProperty LocationsProperty =
        DependencyProperty.Register(
            "Locations", typeof(ReactiveList<LlaPosition>), typeof(TestDraggableMapPolyline),
            new PropertyMetadata(
                null, PropertyChangedCallback));

    private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var locationsCollection = (ReactiveList<LlaPosition>) e.NewValue;

        if (locationsCollection == null)
            return;

        var testDraggableMapPolyline = (TestDraggableMapPolyline) d;
        if (testDraggableMapPolyline == null) return;

        testDraggableMapPolyline.Update();

        locationsCollection.ChangeTrackingEnabled = true;
        locationsCollection.ItemChanged.Subscribe(_T => { testDraggableMapPolyline.Update(); });
    }

    public ReactiveList<LlaPosition> Locations
    {
        get { return (ReactiveList<LlaPosition>)GetValue(LocationsProperty); }
        set { SetValue(LocationsProperty, value); }
    }

    private void Update()
    {
        Panel.InvalidateVisual();
    }

    public TestDraggableMapPolyline()
    {
        InitializeComponent();
    }

    private void ShowHandles(object sender, MouseEventArgs e)
    {
        Handles.Visibility = Visibility.Visible;
    }

    private void HideHandles(object sender, MouseEventArgs e)
    {
        if (!Handles.IsMouseOver)
            Handles.Visibility = Visibility.Hidden;
    }

    private void MoveHandle(object sender, MouseEventArgs e)
    {
        var element = (UIElement)sender;

        if (!element.IsMouseCaptured) return;

        var path = sender as Path;
        var contentPresenter = path?.TemplatedParent as ContentPresenter;
        if (contentPresenter != null)
        {
            var item = contentPresenter.Content as LlaPosition;
            if (item == null)
                return;

            var map = MapPanel.GetParentMap(element);
            var location = map.ViewportPointToLocation(e.GetPosition(map));
            for (var i = 0; i < Handles.Items.Count; i++)
                if (item == Handles.Items[i])
                {
                    Locations[i].Latitude = location.Latitude;
                    Locations[i].Longitude = location.Longitude;
                }
        }
        e.Handled = true;
    }

    private void HandleLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        var element = (UIElement)sender;

        if (Mouse.Capture(element))
            e.Handled = true;
    }

    private void HandleLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        var element = (UIElement)sender;

        if (!element.IsMouseCaptured) return;

        element.ReleaseMouseCapture();
        e.Handled = true;
    }
}
Coordinator
Mar 24 at 10:05 AM
Without having tested your code, I'm sure that
<Setter Property="mapControl:MapPanel.Location" Value="{Binding}" />
in the ItemContainerStyle of the Handles MapItemsControl won't work, because the Location class does not implement INotifyPropertyChanged. So changing a Location's Latitude or Longitude does not trigger the Binding.

You may get around this limitation by creating your own Location class with property change notification support, or by a wrapper class that has a Location property, the value of which is replaced on every move of a handle.
Mar 28 at 1:46 PM
We did indeed substitute our own Location class which implements INotifyPropertyChanged (by inheriting ReactiveObject from the ReactiveUI library, which implements the interface with some helper methods).

That substitution was in place when I posted the above code, so the problem persists. Somehow the handle's position isn't being refreshed even though the MapPolyline is updated fine.
Coordinator
Mar 28 at 3:18 PM
And there is an implicit conversion from LlaPosition to Location?
Mar 28 at 5:28 PM
Edited Mar 28 at 5:32 PM
I actually ended up modifying the whole MapControl codebase to use LlaPosition in lieu of Location. It has Latitude and Longitude as before, and I just mirrored your LocationCollection (now LlaPositionCollection) and XAML parameter conversion infrastructure. So our modified version of your control uses LlaPosition "natively."