Dylan
Dylan

Reputation: 2432

Change the coordinate system of a Canvas in WPF

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:

Upvotes: 18

Views: 26202

Answers (8)

Morten Brask Jensen
Morten Brask Jensen

Reputation: 1

i have nearly the same problem. so i went online. and this guy uses matrix to transform from 'device pixel' to what he calls 'world coordinates' and by that he means real world numbers instead of 'device pixels' see the link

http://csharphelper.com/blog/2014/09/use-transformations-draw-graph-wpf-c/

Upvotes: 0

dharmatech
dharmatech

Reputation: 9497

Here's an answer which describes a Canvas extension method that allows you to apply a Cartesian coordinate system. I.e.:

canvas.SetCoordinateSystem(-10, 10, -10, 10)

will set the coordinate system of canvas so that x goes from -10 to 10 and y goes from -10 to 10.

Upvotes: 0

FTLPhysicsGuy
FTLPhysicsGuy

Reputation: 1065

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.

Upvotes: 0

Nir
Nir

Reputation: 29584

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.

Upvotes: 0

Ryan Lundy
Ryan Lundy

Reputation: 210090

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.

Upvotes: 6

Dylan
Dylan

Reputation: 2432

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.

Upvotes: 1

MojoFilter
MojoFilter

Reputation: 12276

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

Upvotes: 0

MojoFilter
MojoFilter

Reputation: 12276

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)

Upvotes: 0

Related Questions