views:

27

answers:

2

I have to create a canvas that contains object with position in binding with latitude and longitude.

My canvas support element's drag and zooming but until now I have used always standard coordinate (from 0,0)

Now I have GPS coords and all objects overlap each other because GPS coord is for example between 45°11'00'N and 45°11'60N... so is basically 1px of distance if I can't solve this conversion... also canvas starts from 0,0 and I have always 45 pixel of white space

I can retrive minimum left and top values and recalc the size but how can center all the objects in the canvas?

A: 

Was gonna do a blog post on this at some point and may still do. But - in short - I solved this issue just by writing my own panel that understands how to position children according to the mercator projection.

Some things to know before using it:

  • you need to set MaxLatitude and MaxLongitude to whatever range is supported in your background image.
  • I don't think it supports updates in both directions yet. I was planning to fix this prior to my blog post.

You use it like this:

<ListBox ItemsSource="{Binding YourItems}">
    <ListBox.Template>
        <ControlTemplate>
            <Border BorderBrush="Black" BorderThickness="1" Background="#CEE3FF">
                <Grid>
                    <Grid.Resources>
                        <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}">Transparent</SolidColorBrush>
                        <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}">Transparent</SolidColorBrush>
                    </Grid.Resources>
                    <Image x:Name="mapImage" Source="YourMap.png"/>
                    <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" Width="{Binding ActualWidth, ElementName=mapImage}" Height="{Binding ActualHeight, ElementName=mapImage}" />
                </Grid>
            </Border>
        </ControlTemplate>
    </ListBox.Template>
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <!-- make sure you set these values in line with YourMap.png -->
            <controls:MercatorProjectionPanel MaxLatitude="81" MinLatitude="-74"/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem">
            <Setter Property="controls:MercatorProjectionPanel.Longitude" Value="{Binding Location.Longitude}"/>
            <Setter Property="controls:MercatorProjectionPanel.Latitude" Value="{Binding Location.Latitude}"/>
        </Style>
    </ListBox.ItemContainerStyle>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock>Here's your item</TextBlock>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Here's the code:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

/// <summary>
/// Implements a panel that lays out children according to the mercator projection.
/// </summary>
public class MercatorProjectionPanel : Panel
{
    /// <summary>
    /// Identifies the <see cref="MinLatitude"/> dependency property.
    /// </summary>
    public static readonly DependencyProperty MinLatitudeProperty = DependencyProperty.Register(
        "MinLatitude",
        typeof(double),
        typeof(MercatorProjectionPanel),
        new FrameworkPropertyMetadata(DefaultMinLatitude, OnLatitudeRangeChanged));

    /// <summary>
    /// Identifies the <see cref="MaxLatitude"/> dependency property.
    /// </summary>
    public static readonly DependencyProperty MaxLatitudeProperty = DependencyProperty.Register(
        "MaxLatitude",
        typeof(double),
        typeof(MercatorProjectionPanel),
        new FrameworkPropertyMetadata(DefaultMaxLatitude, OnLatitudeRangeChanged));

    /// <summary>
    /// Identifies the <c>Longitude</c> attached dependency property.
    /// </summary>
    public static readonly DependencyProperty LongitudeProperty = DependencyProperty.RegisterAttached(
        "Longitude",
        typeof(double),
        typeof(MercatorProjectionPanel),
        new FrameworkPropertyMetadata(double.NaN, OnGeographicalCoordinateChanged));

    /// <summary>
    /// Identifies the <c>Latitude</c> attached dependency property.
    /// </summary>
    public static readonly DependencyProperty LatitudeProperty = DependencyProperty.RegisterAttached(
        "Latitude",
        typeof(double),
        typeof(MercatorProjectionPanel),
        new FrameworkPropertyMetadata(double.NaN, OnGeographicalCoordinateChanged));

    /// <summary>
    /// Identifies the <c>Left</c> attached dependency property.
    /// </summary>
    public static readonly DependencyProperty LeftProperty = DependencyProperty.RegisterAttached(
        "Left",
        typeof(double),
        typeof(MercatorProjectionPanel),
        new FrameworkPropertyMetadata(double.NaN, OnCoordinateChanged));

    /// <summary>
    /// Identifies the <c>Top</c> attached dependency property.
    /// </summary>
    public static readonly DependencyProperty TopProperty = DependencyProperty.RegisterAttached(
        "Top",
        typeof(double),
        typeof(MercatorProjectionPanel),
        new FrameworkPropertyMetadata(double.NaN, OnCoordinateChanged));

    private static readonly DependencyProperty XRatioProperty = DependencyProperty.RegisterAttached(
        "XRatio",
        typeof(double),
        typeof(MercatorProjectionPanel),
        new FrameworkPropertyMetadata(double.NaN));

    private static readonly DependencyProperty YRatioProperty = DependencyProperty.RegisterAttached(
        "YRatio",
        typeof(double),
        typeof(MercatorProjectionPanel),
        new FrameworkPropertyMetadata(double.NaN));

    private const double DefaultMinLatitude = -80;
    private const double DefaultMaxLatitude = 80;
    private const double DegreesPerRadian = 57.2957;

    private double minY = CalculateYRelative(DefaultMinLatitude);
    private double maxY = CalculateYRelative(DefaultMaxLatitude);

    /// <summary>
    /// Initializes a new instance of the MercatorProjectionPanel class.
    /// </summary>
    public MercatorProjectionPanel()
    {
        SizeChanged += delegate
        {
            InvalidateArrange();
        };
    }

    /// <summary>
    /// Gets or sets the minimum latitude displayed by this mercator projection panel.
    /// </summary>
    public double MinLatitude
    {
        get { return (double)GetValue(MinLatitudeProperty); }
        set { SetValue(MinLatitudeProperty, value); }
    }

    /// <summary>
    /// Gets or sets the maximum latitude displayed by this mercator projection panel.
    /// </summary>
    public double MaxLatitude
    {
        get { return (double)GetValue(MaxLatitudeProperty); }
        set { SetValue(MaxLatitudeProperty, value); }
    }

    /// <summary>
    /// Gets the longitude for a specified dependency object.
    /// </summary>
    /// <param name="dependencyObject">
    /// The dependency object.
    /// </param>
    /// <returns>
    /// The longitude.
    /// </returns>
    public static double GetLongitude(DependencyObject dependencyObject)
    {
        return (double)dependencyObject.GetValue(LongitudeProperty);
    }

    /// <summary>
    /// Sets the longitude for a specified dependency object.
    /// </summary>
    /// <param name="dependencyObject">
    /// The dependency object.
    /// </param>
    /// <param name="longitude">
    /// The longitude.
    /// </param>
    public static void SetLongitude(DependencyObject dependencyObject, double longitude)
    {
        dependencyObject.SetValue(LongitudeProperty, longitude);
    }

    /// <summary>
    /// Gets the latitude for a specified dependency object.
    /// </summary>
    /// <param name="dependencyObject">
    /// The dependency object.
    /// </param>
    /// <returns>
    /// The latitude.
    /// </returns>
    public static double GetLatitude(DependencyObject dependencyObject)
    {
        return (double)dependencyObject.GetValue(LatitudeProperty);
    }

    /// <summary>
    /// Sets the latitude for a specified dependency object.
    /// </summary>
    /// <param name="dependencyObject">
    /// The dependency object.
    /// </param>
    /// <param name="latitude">
    /// The latitude.
    /// </param>
    public static void SetLatitude(DependencyObject dependencyObject, double latitude)
    {
        dependencyObject.SetValue(LatitudeProperty, latitude);
    }

    /// <summary>
    /// Gets the left offset for a specified dependency object.
    /// </summary>
    /// <param name="dependencyObject">
    /// The dependency object.
    /// </param>
    /// <returns>
    /// The left offset.
    /// </returns>
    public static double GetLeft(DependencyObject dependencyObject)
    {
        return (double)dependencyObject.GetValue(LeftProperty);
    }

    /// <summary>
    /// Sets the left offset for a specified dependency object.
    /// </summary>
    /// <param name="dependencyObject">
    /// The dependency object.
    /// </param>
    /// <param name="left">
    /// The left offset.
    /// </param>
    public static void SetLeft(DependencyObject dependencyObject, double left)
    {
        dependencyObject.SetValue(LeftProperty, left);
    }

    /// <summary>
    /// Gets the top offset for a specified dependency object.
    /// </summary>
    /// <param name="dependencyObject">
    /// The dependency object.
    /// </param>
    /// <returns>
    /// The top offset.
    /// </returns>
    public static double GetTop(DependencyObject dependencyObject)
    {
        return (double)dependencyObject.GetValue(TopProperty);
    }

    /// <summary>
    /// Sets the top offset for a specified dependency object.
    /// </summary>
    /// <param name="dependencyObject">
    /// The dependency object.
    /// </param>
    /// <param name="top">
    /// The top offset.
    /// </param>
    public static void SetTop(DependencyObject dependencyObject, double top)
    {
        dependencyObject.SetValue(TopProperty, top);
    }

    /// <summary>
    /// Gets the horizontal alignment for a specified dependency object.
    /// </summary>
    /// <param name="dependencyObject">
    /// The dependency object.
    /// </param>
    /// <returns>
    /// The horizontal alignment.
    /// </returns>
    public static HorizontalAlignment GetHorizontalAlignment(DependencyObject dependencyObject)
    {
        return (HorizontalAlignment)dependencyObject.GetValue(HorizontalAlignmentProperty);
    }

    /// <summary>
    /// Sets the horizontal alignment for a specified dependency object.
    /// </summary>
    /// <param name="dependencyObject">
    /// The dependency object.
    /// </param>
    /// <param name="horizontalAlignment">
    /// The horizontal alignment.
    /// </param>
    public static void SetHorizontalAlignment(DependencyObject dependencyObject, HorizontalAlignment horizontalAlignment)
    {
        dependencyObject.SetValue(HorizontalAlignmentProperty, horizontalAlignment);
    }

    /// <summary>
    /// Gets the vertical alignment for a specified dependency object.
    /// </summary>
    /// <param name="dependencyObject">
    /// The dependency object.
    /// </param>
    /// <returns>
    /// The vertical alignment.
    /// </returns>
    public static VerticalAlignment GetVerticalAlignment(DependencyObject dependencyObject)
    {
        return (VerticalAlignment)dependencyObject.GetValue(VerticalAlignmentProperty);
    }

    /// <summary>
    /// Sets the vertical alignment for a specified dependency object.
    /// </summary>
    /// <param name="dependencyObject">
    /// The dependency object.
    /// </param>
    /// <param name="verticalAlignment">
    /// The vertical alignment.
    /// </param>
    public static void SetVerticalAlignment(DependencyObject dependencyObject, VerticalAlignment verticalAlignment)
    {
        dependencyObject.SetValue(VerticalAlignmentProperty, verticalAlignment);
    }

    /// <summary>
    /// Measures all child controls, imposing no restrictions on their size.
    /// </summary>
    /// <param name="availableSize">
    /// The available size.
    /// </param>
    /// <returns>
    /// The measured size.
    /// </returns>
    protected override Size MeasureOverride(Size availableSize)
    {
        availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);

        foreach (UIElement child in this.InternalChildren)
        {
            if (child != null)
            {
                child.Measure(availableSize);
            }
        }

        return new Size();
    }

    /// <summary>
    /// Arranges all child controls.
    /// </summary>
    /// <param name="finalSize">
    /// The final size.
    /// </param>
    /// <returns>
    /// The size of the content.
    /// </returns>
    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (FrameworkElement child in this.InternalChildren)
        {
            if (child == null)
            {
                continue;
            }

            var xRatio = GetXRatio(child);
            var yRatio = GetYRatio(child);
            var x = xRatio * ActualWidth;
            var y = yRatio * ActualHeight;

            switch (child.HorizontalAlignment)
            {
                case HorizontalAlignment.Center:
                    x -= child.DesiredSize.Width / 2;
                    break;
                case HorizontalAlignment.Right:
                    x -= child.DesiredSize.Width;
                    break;
            }

            switch (child.VerticalAlignment)
            {
                case VerticalAlignment.Center:
                    y -= child.DesiredSize.Height / 2;
                    break;
                case VerticalAlignment.Bottom:
                    y -= child.DesiredSize.Height;
                    break;
            }

            child.Arrange(new Rect(new Point(x, y), child.DesiredSize));
        }

        return finalSize;
    }

    private static double GetXRatio(DependencyObject dependencyObject)
    {
        return (double)dependencyObject.GetValue(XRatioProperty);
    }

    private static void SetXRatio(DependencyObject dependencyObject, double xRatio)
    {
        dependencyObject.SetValue(XRatioProperty, xRatio);
    }

    private static double GetYRatio(DependencyObject dependencyObject)
    {
        return (double)dependencyObject.GetValue(YRatioProperty);
    }

    private static void SetYRatio(DependencyObject dependencyObject, double yRatio)
    {
        dependencyObject.SetValue(YRatioProperty, yRatio);
    }

    private static double CalculateYRelative(double latitude)
    {
        return Math.Log(Math.Tan(((latitude / 360d) * Math.PI) + (Math.PI / 4)));
    }

    private static void OnLatitudeRangeChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var reference = dependencyObject as MercatorProjectionPanel;

        if (reference != null)
        {
            reference.minY = CalculateYRelative(reference.MinLatitude);
            reference.maxY = CalculateYRelative(reference.MaxLatitude);
        }
    }

    private static void OnGeographicalCoordinateChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var reference = dependencyObject as FrameworkElement;

        if (reference != null)
        {
            var parent = VisualTreeHelper.GetParent(reference) as MercatorProjectionPanel;

            if (parent != null)
            {
                SetLeft(reference, ConvertLongitudeToX(parent, GetLongitude(reference)));
                SetTop(reference, ConvertLatitudeToY(parent, GetLatitude(reference)));

                parent.InvalidateArrange();
            }
        }
    }

    private static void OnCoordinateChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var reference = dependencyObject as FrameworkElement;

        if (reference != null)
        {
            var parent = VisualTreeHelper.GetParent(reference) as MercatorProjectionPanel;

            if (parent != null)
            {
                ////SetLongitude(reference, ConvertXToLongitude(parent, GetLeft(reference)));
                ////SetLatitude(reference, ConvertYToLatitude(parent, GetTop(reference)));

                ////parent.InvalidateArrange();

                var left = GetLeft(reference);
                var top = GetTop(reference);

                SetXRatio(reference, left / parent.ActualWidth);
                SetYRatio(reference, top / parent.ActualHeight);
            }
        }
    }

    private static double ConvertXToLongitude(MercatorProjectionPanel panel, double left)
    {
        return ((left / panel.ActualWidth) * 360) - 180;
    }

    private static double ConvertYToLatitude(MercatorProjectionPanel panel, double top)
    {
        var input = panel.maxY - ((top / panel.ActualHeight) * (panel.maxY - panel.minY));
        return Math.Atan(Math.Sinh(input)) * DegreesPerRadian;
    }

    private static double ConvertLongitudeToX(MercatorProjectionPanel panel, double longitude)
    {
        return ((longitude + 180) / 360) * panel.ActualWidth;
    }

    private static double ConvertLatitudeToY(MercatorProjectionPanel panel, double latitude)
    {
        return panel.ActualHeight - (panel.ActualHeight * (CalculateYRelative(latitude) - panel.minY) / (panel.maxY - panel.minY));
    }
}

If I ever get around to the blog post, I'll update this answer.

HTH,
Kent

Kent Boogaart
i'm trying to test your code based on my model, I have created also MinLongitude and MaxLongitude and now i'm searching for a method to get those values from the children (so panel will not be bigger than necessary) but I can't find an entry point in the "rendering" workflow where I can access all children and set Max and Min Latitude and Longitude
Mauro Destro
A: 

You need to also have the lat/long min and max values that you want the canvas to display(that can be computed from the zoom level, or you can hard-code them). Then you can determine the relative coords of an object like: RelX = (ObjectLong-MinLong)/(MaxLong-MinLong) and RelY = (ObjectLat-MinLat)/(MaxLat-MinLat) Then you position the children of the canvas based on their relative coordinates, in the MeasureOverride() method.

Andy