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

Scaling FrameworkElement

Aug 11, 2014 at 5:52 PM
I'm working on placing controls on the map such that they are positioned, sized, and rotated based on their North, South, East, and West bounds. I've added North, South, East, and West as attached properties for FrameworkElement. The North and West are used to set the MapPanel.LocationProperty for positioning.

The FrameworkElement.RenderTransform is set to a TransformGroup containing the MapBase.ScaleRotateTransform. I'm using a transform group to contain the ScaleRotateTransform in order to work properly with the way MapPanel.LocationProperty adds a TranslateTransform.

This all works well except the initial scaling. For a simple test, I'm using an image control and comparing the results to the MapImage in your sample application. The image ends up very small. However it does zoom and rotate appropriately and is in the correct location.
  <Image Source="10_535_330.jpg"
               map:MapElement.South="53.54031"
               map:MapElement.North="53.74871" 
               map:MapElement.West="8.08594"
               map:MapElement.East="8.43750"
               Opacity="0.5" />


public class MapElement
{
    #region Attached Properties

    public static readonly DependencyProperty NorthProperty = DependencyProperty.RegisterAttached(
        "North", typeof(double), typeof(MapElement), new PropertyMetadata(double.NaN, BoundsChanged));

    public static double GetNorth(FrameworkElement element)
    {
        return (double)element.GetValue(NorthProperty);
    }

    public static void SetNorth(FrameworkElement element, double value)
    {
        element.SetValue(NorthProperty, value);
    }

    public static readonly DependencyProperty SouthProperty = DependencyProperty.RegisterAttached(
        "South", typeof(double), typeof(MapElement), new PropertyMetadata(double.NaN, BoundsChanged));

    public static double GetSouth(FrameworkElement element)
    {
        return (double)element.GetValue(SouthProperty);
    }

    public static void SetSouth(FrameworkElement element, double value)
    {
        element.SetValue(SouthProperty, value);
    }

    public static readonly DependencyProperty EastProperty = DependencyProperty.RegisterAttached(
        "East", typeof(double), typeof(MapElement), new PropertyMetadata(double.NaN, BoundsChanged));

    public static double GetEast(FrameworkElement element)
    {
        return (double)element.GetValue(EastProperty);
    }

    public static void SetEast(FrameworkElement element, double value)
    {
        element.SetValue(EastProperty, value);
    }

    public static readonly DependencyProperty WestProperty = DependencyProperty.RegisterAttached(
        "West", typeof(double), typeof(MapElement), new PropertyMetadata(double.NaN, BoundsChanged));

    public static double GetWest(FrameworkElement element)
    {
        return (double)element.GetValue(WestProperty);
    }

    public static void SetWest(FrameworkElement element, double value)
    {
        element.SetValue(WestProperty, value);
    }

    #endregion

    public static bool AreBoundsValid(FrameworkElement element)
    {
        double north = GetNorth(element);
        double south = GetSouth(element);
        double east = GetEast(element);
        double west = GetWest(element);

        return !double.IsNaN(south) && !double.IsNaN(north)
            && !double.IsNaN(west) && !double.IsNaN(east)
            && south < north && west < east;
    }

    private static void BoundsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs eventArgs)
    {
        FrameworkElement element = sender as FrameworkElement;
        if (element == null)
            return;
        IMapElement mapElement = element as IMapElement;
        MapBase parentMap = mapElement != null ? mapElement.ParentMap : MapPanel.GetParentMap(element);
        if (parentMap == null)
        {
            return;
        }

        if (AreBoundsValid(element))
        {
            Location northWest = new Location(GetNorth(element), GetWest(element));
            Point topLeft = parentMap.LocationToViewportPoint(new Location(GetNorth(element), GetWest(element)));
            Point bottomRight = parentMap.LocationToViewportPoint(new Location(GetSouth(element), GetEast(element)));
            Vector viewportSize = bottomRight - topLeft;
            element.Height = viewportSize.Y;
            element.Width = viewportSize.X;
            TransformGroup transformGroup = new TransformGroup();
            transformGroup.Children.Add(parentMap.ScaleRotateTransform);
            element.RenderTransform = transformGroup;
            element.SetValue(MapPanel.LocationProperty, northWest);
        }
    }
}
What is the coordinate space in which the height and width should be calculated?

Thanks,
Rob
Coordinator
Aug 11, 2014 at 7:08 PM
Edited Aug 11, 2014 at 7:08 PM
I'm afraid I don't understand the question. Why would you set Width and Height of a FrameworkElement, and also its RenderTransform? Shouldn't the Width and Height be constant, and the effective size only be determined by the RenderTransform?
Aug 11, 2014 at 7:29 PM
I agree, the height an width should be constant, and the RenderTransform scales it to the effective size determined by the zoom. I'm trying to determine the height and width only once (or any time the bounds change). The height and width must have value before the RenderTransform is applied.

In the above example, I tried calculating the height and width using LocationToViewportPoint:
        Point topLeft = parentMap.LocationToViewportPoint(new Location(GetNorth(element), GetWest(element)));
        Point bottomRight = parentMap.LocationToViewportPoint(new Location(GetSouth(element), GetEast(element)));
        Vector viewportSize = bottomRight - topLeft;
        element.Height = viewportSize.Y;
        element.Width = viewportSize.X;
This results in a height of 511.99... and a width of 511.99... After the render transform is applied, the rendered size is approximately 1/45th the size of the equivalent MapRectangle from the sample app. I'm trying to make it exactly the same size as the MapRectangle from the sample app.
Coordinator
Aug 11, 2014 at 7:35 PM
Edited Aug 11, 2014 at 7:41 PM
ScaleRotateTransform is the wrong transform if you want to convert from viewport coordinates to an appropriate scaling. It is (from code documentation) the combination of ScaleTransform and RotateTransform, where ScaleTransform is the scaling transformation from meters to viewport coordinate units (pixels).

You may perhaps convert the FrameworkElement "real world" bounds to meters instead of viewport coordinates before using ScaleRotateTransform.
Aug 11, 2014 at 8:37 PM
Awesome, thanks for pointing me in the right direction. This seems to work well:
    private static void BoundsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs eventArgs)
    {
        FrameworkElement element = sender as FrameworkElement;
        if (element == null)
            return;
        IMapElement mapElement = element as IMapElement;
        MapBase parentMap = mapElement != null ? mapElement.ParentMap : MapPanel.GetParentMap(element);
        if (parentMap == null)
        {
            return;
        }

        if (AreBoundsValid(element))
        {
            Location northWest = new Location(GetNorth(element), GetWest(element));
            Location southWest = new Location(GetSouth(element), GetWest(element));

            Point bottomLeft = parentMap.MapTransform.Transform(southWest);
            Point topRight = parentMap.MapTransform.Transform(new Location(GetNorth(element), GetEast(element)));
            double relativeScale = parentMap.MapTransform.RelativeScale(southWest);
            Vector viewportSize = (topRight - bottomLeft) * TileSource.MetersPerDegree / relativeScale;
            element.Height = viewportSize.Y;
            element.Width = viewportSize.X;

            TransformGroup transformGroup = new TransformGroup();
            transformGroup.Children.Add(parentMap.ScaleRotateTransform);
            element.RenderTransform = transformGroup;
            element.SetValue(MapPanel.LocationProperty, northWest);
        }
    }
Aug 12, 2014 at 1:22 PM
Following up with my results. The above works fine if the aspect ratio of the image matches the aspect ratio of the bounds specified. However, if the they do not match, the image will not stretch appropriately. You can see this by changing the sample code to:
<Image Source="10_535_330.jpg"
            map:MapElement.South="53.54031"
            map:MapElement.North="53.74871" 
            map:MapElement.West="8.08"
            map:MapElement.East="8.43750"
            Opacity="0.5" />
As you zoom, the image will slide horizontally across the map. To fix the issue, set the Image's Fill.
<Image Source="10_535_330.jpg"
             map:MapElement.South="53.54031"
             map:MapElement.North="53.74871" 
             map:MapElement.West="8.08"
             map:MapElement.East="8.43750"
             Opacity="0.5" Stretch="Fill" />
Now the image stays in place as you zoom, pan, and rotate.
Aug 12, 2014 at 7:29 PM
I thought I had it, but when I stretch the image over a larger area it becomes clear that I'm not using the ScaleTransform appropriately.
   <Image Source="10_535_330.jpg" Stretch="Fill"
               map:MapElement.South="-2"
               map:MapElement.North="2"
               map:MapElement.West="-2"
               map:MapElement.East="2" />
When moving the map vertically the Image gets larger and smaller. It looks like the ScaleTransform changes based on the center latitude which is causing the change in size.

I've experimented with the ViewportTransform with little success. The ViewportTrasnform performs a translation which I think is doubling the translation added by the MapPanel.Location.

Do you have any suggestions on an alternative to the ScaleTransform?

Thanks,
Rob
Coordinator
Aug 12, 2014 at 7:53 PM
Edited Aug 12, 2014 at 7:54 PM
I'm afraid not. You are actually trying to do an impossible thing. The projections of the corner points of your Image control can not be converted to a common scale transform, because the map transform (a Mercator projection) is not an affine transform.

The best you can get is an approximation, i.e. a scale factor at some common point in the current viewport. Map.ScaleTransform uses the viewport's center point to calculate its scaling factor.
Coordinator
Aug 12, 2014 at 8:37 PM
Edited Aug 12, 2014 at 8:39 PM
You might try this calculation:
var center = new Location((south + north) / 2d, (west + east) / 2d);
var height = (north - south) * TileSource.MetersPerDegree;
var width = (east - west) * TileSource.MetersPerDegree
          / map.MapTransform.RelativeScale(center);
var scale = map.GetMapScale(center);
var transform = new TransformGroup();
transform.Children.Add(new ScaleTransform(scale, scale));

element.Width = width;
element.Height = height;
element.RenderTransform = transform;
MapPanel.SetLocation(element, new Location(north, west));
Aug 12, 2014 at 8:51 PM
Edited Aug 12, 2014 at 9:02 PM
Thanks, this scales it perfectly at the initial zoom level. However the image does not scale or rotate with the zoom and rotation of the map.

Edit:
Adding transform.Children.Add(parentMap.RotateTransform); fixes the rotation.
Coordinator
Aug 12, 2014 at 9:03 PM
Edited Aug 12, 2014 at 9:05 PM
No, of course it doesn't . This was just meant to give you an idea how the scale transform calculation would be performed correctly.

It should be quite easy to add the Map's RotateTransform to the TransformGroup. However, you would need to recalculate the ScaleTransform every time the Map's Viewport changes. There is a ViewportChanged event for this purpose.

Or you just live with the approximation of ScaleRotateTransform at the viewport center:
var transform = new TransformGroup();
transform.Children.Add(map.ScaleRotateTransform); // instead of new ScaleTransform
Aug 13, 2014 at 2:37 PM
Ok, I stepped back and attacked the problem from a very different angle. Restating my original goal: stretch any FrameworkElement to fit in the bounds of a rectangular area described by the area's north and south latitude and east and west longitude. It should behave similar to MapImage, but work for any control.

I implemented the following MapCanvas control to contain my other controls. Within the MapCanvas control the canvas coordinates are specified in the map controls MapTransform space. Attaching properties MapCanvas.North, South, East, and West will be converted into the Canvast Top Left Height and Width. The Canvas is transformed using the ViewportTransform. Controls do not need to be transformed individually.
public class MapCanvas : Canvas, IMapElement
{
    private const FrameworkPropertyMetadataOptions options = FrameworkPropertyMetadataOptions.AffectsMeasure
        | FrameworkPropertyMetadataOptions.AffectsArrange
        | FrameworkPropertyMetadataOptions.AffectsRender;

    #region Attached Properties

    public static readonly DependencyProperty NorthProperty = DependencyProperty.RegisterAttached(
        "North", typeof(double), typeof(MapCanvas),
        new FrameworkPropertyMetadata(double.NaN, options, BoundsChanged));

    public static double GetNorth(FrameworkElement element)
    {
        return (double)element.GetValue(NorthProperty);
    }

    public static void SetNorth(FrameworkElement element, double value)
    {
        element.SetValue(NorthProperty, value);
    }

    public static readonly DependencyProperty SouthProperty = DependencyProperty.RegisterAttached(
        "South", typeof(double), typeof(MapCanvas),
        new FrameworkPropertyMetadata(double.NaN, options, BoundsChanged));

    public static double GetSouth(FrameworkElement element)
    {
        return (double)element.GetValue(SouthProperty);
    }

    public static void SetSouth(FrameworkElement element, double value)
    {
        element.SetValue(SouthProperty, value);
    }

    public static readonly DependencyProperty EastProperty = DependencyProperty.RegisterAttached(
        "East", typeof(double), typeof(MapCanvas),
        new FrameworkPropertyMetadata(double.NaN, options, BoundsChanged));

    public static double GetEast(FrameworkElement element)
    {
        return (double)element.GetValue(EastProperty);
    }

    public static void SetEast(FrameworkElement element, double value)
    {
        element.SetValue(EastProperty, value);
    }

    public static readonly DependencyProperty WestProperty = DependencyProperty.RegisterAttached(
        "West", typeof(double), typeof(MapCanvas),
        new FrameworkPropertyMetadata(double.NaN, options, BoundsChanged));

    public static double GetWest(FrameworkElement element)
    {
        return (double)element.GetValue(WestProperty);
    }

    public static void SetWest(FrameworkElement element, double value)
    {
        element.SetValue(WestProperty, value);
    }

    #endregion

    #region Properties

    public MapBase ParentMap { get; set; }

    #endregion

    public MapCanvas()
    {
        this.ParentMap = MapPanel.GetParentMap(this);
    }

    public static bool AreBoundsValid(FrameworkElement element)
    {
        double north = GetNorth(element);
        double south = GetSouth(element);
        double east = GetEast(element);
        double west = GetWest(element);

        return !double.IsNaN(south) && !double.IsNaN(north)
            && !double.IsNaN(west) && !double.IsNaN(east)
            && south < north && west < east;
    }

    private static void BoundsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs eventArgs)
    {
        FrameworkElement element = sender as FrameworkElement;
        if (element == null)
            return;
        IMapElement mapElement = element as IMapElement;
        MapBase parentMap = mapElement != null ? mapElement.ParentMap : MapPanel.GetParentMap(element);
        if (parentMap == null)
        {
            return;
        }

        if (AreBoundsValid(element))
        {
            Location northEast = new Location(GetNorth(element), GetEast(element));
            Location southWest = new Location(GetSouth(element), GetWest(element));

            Point topRight = parentMap.MapTransform.Transform(northEast);
            Point bottomLeft = parentMap.MapTransform.Transform(southWest);

            Rect rect = new Rect(topRight, bottomLeft);
            element.SetValue(Canvas.TopProperty, topRight.Y);
            element.SetValue(Canvas.LeftProperty, bottomLeft.X);
            element.SetValue(FrameworkElement.HeightProperty, rect.Height);
            element.SetValue(FrameworkElement.WidthProperty, rect.Width);

            // ViewportTransform includes a vertical flip which must be countered
            // More work needed to handle other transforms set before this point
            element.RenderTransform = new MatrixTransform(1, 0, 0, -1, 0, 0);
        }
    }

    public void SetParentMap(MapBase parentMap)
    {
        this.ParentMap = parentMap;

        if (this.ParentMap != null)
        {
            this.RenderTransform = this.ParentMap.ViewportTransform;
        }
    }
}
To use the MapCanvas, add a single MapCanvas as a child of the Map. Then add your controls within the MapCanvas.
        <map:MapCanvas>
            <Image x:Name="mapImage" Source="10_535_330.jpg" Stretch="Fill" Opacity="0.5"
                   map:MapCanvas.South="53.54031"
                   map:MapCanvas.North="53.74871"
                   map:MapCanvas.West="8.08594"
                   map:MapCanvas.East="8.43750" />

            <Image Source="10_535_330.jpg" Stretch="Fill" Opacity="0.5"
                   map:MapCanvas.South="-10"
                   map:MapCanvas.North="10"
                   map:MapCanvas.West="-10"
                   map:MapCanvas.East="10" />
        </map:MapCanvas>

Thanks for all of your help and for this excellent map control.
Aug 20, 2014 at 6:39 PM
don't forget to give the mapCanvas a name in the xaml

<map:MapCanvas x:Name="MapCanvas">


and call the SetParent method in the code behind

MapCanvas.SetParentMap(map);


At least i think you have to.

thanks rgravesdg for the excellent sample. This gets me half way to solving a different problem i was having.
Aug 20, 2014 at 6:44 PM
I think the SetParentMap method is called automatically for all map children that implement IMapElement.
Coordinator
Aug 20, 2014 at 6:55 PM
That's right, it's usually never necessary to call SetParentMap in your application code. Just add your control as child of a Map control.
Aug 21, 2014 at 1:50 PM
That's odd, because when i comment out this line:
//MapCanvas.SetParentMap(map);

The canvas stops working (no images render).
Aug 21, 2014 at 1:58 PM
What version are you using? With Version 2.0 (July 1st I think), IMapElement changed to include the method SetParentMap. Prior to version 2.0 it had a setter for the ParentMap property instead.
Aug 21, 2014 at 3:21 PM
Turns out I was working with the 2.1 version of the NuGet package. I didn't realize there was an update. It works as your describe now. Thanks
Aug 26, 2014 at 3:49 PM
This may belong in a new thread but I wanted to post it here because I am extending the MapCanvas example...

I tried adding resizing-adorners the images in the mapCanvas so i could size/position elements and save them back out. They work but the adorners themselves are being scaled along with the image (see attached image). If the image is smaller than several hundred miles across than the adorners don't even paint. Any ideas how to overcome this problem? I don't see any properties of the adorner class to set.

Image
Nov 3, 2014 at 6:01 PM
Hello,

I am trying to use your example to have the possibility to add custom control to the map, thank you for your contribution. Now, we changed the mapcontrol since we needed to use a Cartesian and linear content representation, but this part of the code should not change. In my case your code is not working. Moreover, I cannot understand it. My question is: why the method BoundsChanged is not taking into consideration the viewporttransform? This way the size is not changing if I change zoom and center. It does make sense for you?
BTW, with this mapcontrol it is very easy to set a control in a location using attached property. Really good work! Unfortunately, it is not clear to me how to set a dimension proportionally to the map scale. I used the MapRectangle code and it is working, unfortunately the zoom basic ratio is very huge (content appears very tiny) and this is not good to us.
Thank you.

Daniel