tags:

views:

3239

answers:

6

I'm writing a mapping app that uses a Canvas for positioning elements. For each element I have to programatically convert element's Lat/Long to the canvas' coordinate, then set the Canvas.Top and Canvas.Left properties.

If I had a 360x180 Canvas, can I convert the coordinates on the canvas to go from -180 to 180 rather than 0 to 360 on the X axis and 90 to -90 rather than 0 to 180 on the Y axis?

Scaling requirements:

  • The canvas can be any size, so should still work if it's 360x180 or 5000x100.
  • The Lat/Long area may not always be (-90,-180)x(90,180), it could be anything (ie (5,-175)x(89,-174)).
  • Elements such as PathGeometry which are point base, rather than Canvas.Top/Left based need to work.
A: 

I'm pretty sure you can't do that exactly, but it would be pretty trivial to have a method which translated from lat/long to Canvas coordinates.

Point ToCanvas(double lat, double lon) {
  double x = ((lon * myCanvas.ActualWidth) / 360.0) - 180.0;
  double y = ((lat * myCanvas.ActualHeight) / 180.0) - 90.0;
  return new Point(x,y);
}

(Or something along those lines)

MojoFilter
I have that function, I was just hoping to not need to convert points to and from the canvas all the time.
Dylan
Doing it that way does have the added benefit of making your canvas nice and scalable.
MojoFilter
A: 

I guess another option would be to extend canvas and override the measure / arrange to make it behave the way you want.

MojoFilter
That's more along the lines I was thinking, but I'm not sure how to implement it.
Dylan
It's not really tooooo simple. The idea is that you override ArrangeOverride() and MeasureOverride().
MojoFilter
A: 

I was able to get it to by creating my own custom canvas and overriding the ArrangeOverride function like so:

    public class CustomCanvas : Canvas
    {
        protected override Size ArrangeOverride(Size arrangeSize)
        {
            foreach (UIElement child in InternalChildren)
            {
                double left = Canvas.GetLeft(child);
                double top = Canvas.GetTop(child);
                Point canvasPoint = ToCanvas(top, left);
                child.Arrange(new Rect(canvasPoint, child.DesiredSize));
            }
            return arrangeSize;
        }
        Point ToCanvas(double lat, double lon)
        {
            double x = this.Width / 360;
            x *= (lon - -180);
            double y = this.Height / 180;
            y *= -(lat + -90);
            return new Point(x, y);
        }
    }

Which works for my described problem, but it probably would not work for another need I have, which is a PathGeometry. It wouldn't work because the points are not defined as Top and Left, but as actual points.

Dylan
+2  A: 

Here's an all-XAML solution. Well, mostly XAML, because you have to have the IValueConverter in code. So: Create a new WPF project and add a class to it. The class is MultiplyConverter:

namespace YourProject
{
    public class MultiplyConverter : System.Windows.Data.IValueConverter
    {
        public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return AsDouble(value)* AsDouble(parameter);
        }
        double AsDouble(object value)
        {
            var valueText = value as string;
            if (valueText != null)
                return double.Parse(valueText);
            else
                return (double)value;
        }

        public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new System.NotSupportedException();
        }
    }
}

Then use this XAML for your Window. Now you should see the results right in your XAML preview window.

EDIT: You can fix the Background problem by putting your Canvas inside another Canvas. Kind of weird, but it works. In addition, I've added a ScaleTransform which flips the Y-axis so that positive Y is up and negative is down. Note carefully which Names go where:

<Canvas Name="canvas" Background="Moccasin">
    <Canvas Name="innerCanvas">
        <Canvas.RenderTransform>
            <TransformGroup>
                <TranslateTransform x:Name="translate">
                    <TranslateTransform.X>
                        <Binding ElementName="canvas" Path="ActualWidth"
                                Converter="{StaticResource multiplyConverter}" ConverterParameter="0.5" />
                    </TranslateTransform.X>
                    <TranslateTransform.Y>
                        <Binding ElementName="canvas" Path="ActualHeight"
                                Converter="{StaticResource multiplyConverter}" ConverterParameter="0.5" />
                    </TranslateTransform.Y>
                </TranslateTransform>
                <ScaleTransform ScaleX="1" ScaleY="-1" CenterX="{Binding ElementName=translate,Path=X}"
                        CenterY="{Binding ElementName=translate,Path=Y}" />
            </TransformGroup>
        </Canvas.RenderTransform>
        <Rectangle Canvas.Top="-50" Canvas.Left="-50" Height="100" Width="200" Fill="Blue" />
        <Rectangle Canvas.Top="0" Canvas.Left="0" Height="200" Width="100" Fill="Green" />
        <Rectangle Canvas.Top="-25" Canvas.Left="-25" Height="50" Width="50" Fill="HotPink" />
    </Canvas>
</Canvas>

As for your new requirements that you need varying ranges, a more complex ValueConverter would probably do the trick.

Kyralessa
This solution also works with a PathGeometry, by the way.
Kyralessa
This is pretty close to what I need, but I don't think that this scales well enough. If either the coordinate system was Lat=(2, -74) Lon=(-180, -175) or if the size of the Canvas changes (h=50,w=100) it will not work. Also, the Background property is unusable.
Dylan
The various Transforms are what you want, but finding the exact right combination and order of them is kind of a pain.
Kyralessa
...and it's not made any easier by the fact that the XAML preview window often doesn't show things to scale. If something that should be correct looks off in the preview window, you may have to run the app to see that it's not really off.
Kyralessa
A: 

You can use transform to translate between the coordinate systems, maybe a TransformGroup with a TranslateTranform to move (0,0) to the center of the canvas and a ScaleTransform to get the coordinates to the right range.

With data binding and maybe a value converter or two you can get the transforms to update automatically based on the canvas size.

The advantage of this is that it will work for any element (including a PathGeometry), a possible disadvantage is that it will scale everything and not just points - so it will change the size of icons and text on the map.

Nir
A: 

Another possible solution:

Embed a custom canvas (the draw-to canvas) in another canvas (the background canvas) and set the draw-to canvas so that it is transparent and does not clip to bounds. Transform the draw-to canvas with a matrix that makes y flip (M22 = -1) and translates/scales the canvas inside the parent canvas to view the extend of the world you're looking at.

In effect, if you draw at -115, 42 in the draw-to canvas, the item you are drawing is "off" the canvas, but shows up anyway because the canvas is not clipping to bounds. You then transform the draw-to canvas so that the point shows up in the right spot on the background canvas.

This is something I'll be trying myself soon. Hope it helps.

FTLPhysicsGuy
Note: I tried this out, but found one oddity. If you scale in until a meter is roughly a few tens of pixels and you are NOT near the actual 0,0 point of the draw-to canvas, moving the draw-to canvas (via transform OR via Canvas.SetTop/SetLeft) seems to be quantized. Even though you change either top/left or M11/M22 by small values, the visual drawing doesn't move except at "quantized" values (not sure what those values are). I'm wondering if it's making calculations in double but passing them to some lower-level routine that converts to floats, thus "quantizing" large numbers.
FTLPhysicsGuy