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