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

MapPolyline: Arrowheads

Apr 5, 2014 at 12:23 PM
I would like to be able to display an arrow-head at the final location of a polyline. Is this supported as I see nothing within the available properties?

Thanks.

M.
Coordinator
Apr 5, 2014 at 12:36 PM
MapPolyline derives from the WPF Shape class. Hence it provides the common Pen-related properties like StrokeStartLineCap and StrokeEndLineCap, but nothing like an arrow-head. You would have to add an additional object (e.g. a Path) and set the MapPanel.Location attached property.
Apr 7, 2014 at 2:10 PM
I have tried adapting Charles Petzold's line drawing with arrow heads from here and adding this to your own MapPolyline code, however I am struggling to see how I can maintain a fixed size for the arrow head when the map zooms in and out. My code is as follows...
class MapWindowPolyline : MapPath {

    #region Fields

    public static readonly DependencyProperty ArrowAngleProperty = DependencyProperty.Register("ArrowAngle",
        typeof(double), typeof(MapWindowPolyline), new FrameworkPropertyMetadata(45.0, FrameworkPropertyMetadataOptions.AffectsMeasure));

    public static readonly DependencyProperty ArrowLengthProperty = DependencyProperty.Register("ArrowLength",
        typeof(double), typeof(MapWindowPolyline), new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.AffectsMeasure));

    public static readonly DependencyProperty IsArrowClosedProperty = DependencyProperty.Register("IsArrowClosed",
        typeof(bool), typeof(MapWindowPolyline), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure));

    public static readonly DependencyProperty IsClosedProperty = DependencyProperty.Register("IsClosed",
        typeof(Boolean), typeof(MapWindowPolyline), new PropertyMetadata(false, (d, e) => ((MapWindowPolyline)d).UpdateData()));

    public static readonly DependencyProperty LocationsProperty = DependencyProperty.Register("Locations",
        typeof(IEnumerable<Location>), typeof(MapWindowPolyline), new PropertyMetadata(null, MapWindowPolyline.OnLocationsPropertyChanged));

    #endregion

    #region Constructor

    public MapWindowPolyline() {
        this.Data = new PathGeometry();
    }

    #endregion

    protected override void OnVisualParentChanged(DependencyObject oldParent) {
        base.OnVisualParentChanged(oldParent);

        if (this.ParentMap != null) {
            this.ParentMap.ViewportChanged += this.OnParentMapViewportChanged;
        }
    }

    #region Properties

    public double ArrowAngle {
        set { SetValue(MapWindowPolyline.ArrowAngleProperty, value); }
        get { return (double)GetValue(MapWindowPolyline.ArrowAngleProperty); }
    }

    public double ArrowLength {
        set { SetValue(MapWindowPolyline.ArrowLengthProperty, value); }
        get { return (double)GetValue(MapWindowPolyline.ArrowLengthProperty); }
    }

    public bool IsArrowClosed {
        set { SetValue(MapWindowPolyline.IsArrowClosedProperty, value); }
        get { return (bool)GetValue(MapWindowPolyline.IsArrowClosedProperty); }
    }

    public Boolean IsClosed {
        get { return (bool)GetValue(MapWindowPolyline.IsClosedProperty); }
        set { SetValue(MapWindowPolyline.IsClosedProperty, value); }
    }

    [TypeConverter(typeof(LocationCollectionConverter))]
    public IEnumerable<Location> Locations {
        get { return (IEnumerable<Location>)GetValue(MapWindowPolyline.LocationsProperty); }
        set { SetValue(MapWindowPolyline.LocationsProperty, value); }
    }

    #endregion

    #region Methods

    private void OnLocationCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
        this.UpdateData();
    }

    private static void OnLocationsPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) {

        var mapPolyline = (MapWindowPolyline)obj;
        var oldCollection = e.OldValue as INotifyCollectionChanged;
        var newCollection = e.NewValue as INotifyCollectionChanged;

        if (oldCollection != null)
            oldCollection.CollectionChanged -= mapPolyline.OnLocationCollectionChanged;

        if (newCollection != null)
            newCollection.CollectionChanged += mapPolyline.OnLocationCollectionChanged;

        mapPolyline.UpdateData();

    }

    void OnParentMapViewportChanged(object sender, EventArgs e) {
        this.UpdateData();
    }

    protected override void UpdateData() {
        base.UpdateData();

        IList<Location> locations = this.Locations as IList<Location>;
        if (locations == null && this.Locations != null)
            locations = this.Locations.ToList();

        PathGeometry geometry = this.Data as PathGeometry;
        if (this.ParentMap != null && locations != null && locations.Count > 1) {

            PathFigure lineFigure = new PathFigure();
            PolyLineSegment lineSegment = new PolyLineSegment();
            lineFigure.Segments.Add(lineSegment);

            lineFigure.StartPoint = this.ParentMap.MapTransform.Transform(locations.First());
            for (Int32 i = 1; i < locations.Count; i++)
                lineSegment.Points.Add(this.ParentMap.MapTransform.Transform(locations[i]));

            geometry.Figures.Add(lineFigure);

            PathFigure arrowHeadFigure = new PathFigure();
            PolyLineSegment arrowHeadSegment = new PolyLineSegment();
            arrowHeadFigure.Segments.Add(arrowHeadSegment);

            Matrix matrix = new Matrix();
            Point frPoint = new Point(locations[locations.Count - 2].Longitude, locations[locations.Count - 2].Latitude),
                toPoint = new Point(locations[locations.Count - 1].Longitude, locations[locations.Count - 1].Latitude);
            Vector vector = frPoint - toPoint;
            vector.Normalize();

            Double zoomLevel = Math.Min(this.ParentMap.ZoomLevel, 7.5);
            vector *= (this.ArrowLength / (zoomLevel * zoomLevel));

            matrix.Rotate(this.ArrowAngle / 2);
            Point p = toPoint + vector * matrix;
            arrowHeadFigure.StartPoint = this.ParentMap.MapTransform.Transform(new Location(p.Y, p.X));
            arrowHeadSegment.Points.Add(this.ParentMap.MapTransform.Transform(locations[locations.Count - 1]));

            matrix.Rotate(-this.ArrowAngle);
            p = toPoint + vector * matrix;
            arrowHeadSegment.Points.Add(this.ParentMap.MapTransform.Transform(new Location(p.Y, p.X)));

            geometry.Figures.Add(arrowHeadFigure);

            geometry.Transform = ParentMap.ViewportTransform;

        }

    }

    #endregion

}
As you will see, within the UpdateData method I am trying to manipulate the size of the arrow head using the current map zoom level; ideally, I would like to be able to say that irrespective of the zoom level, the tails are always a fixed size but I cannot see a way to set this size such that it works with the later transform. Can you provide any further advise?

Thanks.
Coordinator
Apr 7, 2014 at 3:09 PM
As said before, I suggest to put an additional Path on the map which draws only the arrowhead. It would not scale, but just change its position by means of the MapPanel.Location attached property.
Apr 7, 2014 at 4:53 PM
I appreciate your comment regarding a path with the MapPanel.Location attached property; problem is I do not really understand where you are heading with that route. If I am right, that provides a means of placing a visual at a specific lat/lon on the map?

I have however overcome the problem by drawing the arrow heads separately from the lines; this allows me to position the arrow heads using this.ParentMap.LocationToViewportPoint and because I am no longer setting a Transform those arrow heads do not resize.

In case this is any help to others, my revised UpdateData code is as follows...
if (locations.Count > 1) {

    PathFigure arrowHeadFigure = new PathFigure();
    PolyLineSegment arrowHeadSegment = new PolyLineSegment();
    arrowHeadFigure.Segments.Add(arrowHeadSegment);

    Matrix matrix = new Matrix();
    Point frPoint = this.ParentMap.LocationToViewportPoint(locations[locations.Count - 2]),
        toPoint = this.ParentMap.LocationToViewportPoint(locations[locations.Count - 1]);
    Vector vector = frPoint - toPoint;
    vector.Normalize();

    vector *= this.ArrowLength;

    matrix.Rotate(this.ArrowAngle / 2);
    Point p = toPoint + vector * matrix;
    arrowHeadFigure.StartPoint = p;
    arrowHeadSegment.Points.Add(toPoint);

    matrix.Rotate(-this.ArrowAngle);
    p = toPoint + vector * matrix;
    arrowHeadSegment.Points.Add(p);

    geometry.Figures.Add(arrowHeadFigure);

}
This methodology also allows me to set the angles of the arrow heads appropriately as I know the point the line is coming from and thus the angle of the original line.

Note: "locations" is IList<Location>.

Martin.
Marked as answer by MartinRobins on 11/26/2014 at 7:23 AM
Jun 16, 2015 at 3:02 PM
Edited Jun 16, 2015 at 3:02 PM
ClemensF wrote:
As said before, I suggest to put an additional Path on the map which draws only the arrowhead. It would not scale, but just change its position by means of the MapPanel.Location attached property.
Hi ClemensF,

Per your suggestion, I've placed an additional Path on the map which draws only the arrowhead, but how can I rotate it so it points to the MapPolyline (vector) direction?
Jun 19, 2015 at 5:17 PM
Hi Clemens, I'll greatly appreciate your help on this. How can this be done right?
Jul 1, 2015 at 9:01 PM
I'm really struggling with this one.

Each arrowhead (except for the first one) is related to a specific Location, and should be positioned along the vector between that Location and the previous Location (of the previous child element), at a pixel-offset (say 40 pixels) from the Location point. AFAICT, this means MapBase.ViewportTransform can't be used for this.

Any ideas?
Coordinator
Jul 2, 2015 at 6:58 AM
Create an item (view model) class with Location and Angle properties. Put a collection of these items in a MapItemsControl, bind their Location in the ItemContainerStyle, and have an ItemTemplate with some triangle with a RotateTransform that binds to the Angle property.
Jul 2, 2015 at 12:06 PM
Thanks Clemens!

You've led me to the solution, I've defined a VectoredLocation (Location+Angle) and a IMultiValueConverter which gets the map itself (since ConverterParameter can't bind) and a collection of my GeoPosition, and returns a collection of VectoredLocation to be used in the template:
<Style x:Key="WaypointVectorItemStyle" TargetType="map:MapItem">
    <Setter Property="map:MapPanel.Location" Value="{Binding Location}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="map:MapItem">
                <Canvas RenderTransformOrigin=".5,.5">
                    <Canvas.RenderTransform>
                        <RotateTransform Angle="{Binding Angle}"/>
                    </Canvas.RenderTransform>
                    <Polygon Canvas.Left="24" Points="-9,0 9,7 9,-7" Fill="Yellow"/>
                </Canvas>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
Passing the map itself using MultiBinding feels like a hack, but it works.

Thanks again.