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

Zoom to fit

Nov 12, 2013 at 7:44 PM
Hi,

Firstly thanks for providing a great control, I'm in need of a little help though.

I am adding a number of markers to the map (custom control) and would like to zoom the map to fit them. I'm able to calculate the max/min long/lat that represents the boundary of all the markers but I'm stuck with the zoom level.

I can obviously convert the top left & bottom right of the virtual rectangle to viewport co-ordinates but how do I know for a given zoom if those points are visible?

I hope I've not missed anything silly? Appreciate any help you can throw this way.

Thanks

Bob.
Coordinator
Nov 12, 2013 at 10:17 PM
Edited Nov 13, 2013 at 9:56 AM
This task isn't really trivial, but as long as there is no map rotation you could calculate the new map center and zoom level in a straightforward way.

In order to do that you would have to convert the target bounds from world coordinates to logical map coordinates (by means of the Mercator transform provided by the MapBase.MapTransform property). Dividing the current viewport width and height by the width and height of the bounds in logical coordinates, and multiplying these values with 360 degrees divided by 256 pixels, gives you the viewport scale factor in horizontal and vertical direction respectively. The zoom levels for each direction are now calculated as logarithm to base 2 of these scale factors. As you would only zoom such that all markers are visible, the resulting overall zoom level would be the minimum of these two values.

The following extension method should do all this:
public static class MapEx
{
    public static void ZoomToBounds(this Map map,
        double west, double south, double east, double north)
    {
        var p1 = map.MapTransform.Transform(new Location(south, west));
        var p2 = map.MapTransform.Transform(new Location(north, east));
        var lonScale = map.ActualWidth / (p2.X - p1.X) * 360d / 256d;
        var latScale = map.ActualHeight / (p2.Y - p1.Y) * 360d / 256d;
        var lonZoom = Math.Log(lonScale, 2d);
        var latZoom = Math.Log(latScale, 2d);

        map.TargetZoomLevel = Math.Min(lonZoom, latZoom);
        map.TargetCenter = map.MapTransform.Transform(
            new Point((p1.X + p2.X) / 2d, (p1.Y + p2.Y) / 2d));
    }
}
Marked as answer by ironsidebod on 11/13/2013 at 8:24 AM
Nov 13, 2013 at 3:24 PM
Thankyou!

Rotation isn't a problem as it will be controlled by the application. I had some code very similar to above but the key bit I didn't know about was the Transform.

Combined with my bounds checking and your code above it's all working nicely.

Cheers again.

Bob.
Coordinator
Nov 18, 2013 at 8:42 AM
I've added a ZoomToBounds method to class MapBase in version 1.9.0.
Jun 8, 2015 at 9:36 PM
Hey ClemensF,

I see that the logic hasn't really changed in ZoomToBounds since this conversation took place, but I'd like to revisit it, if you don't mind. I'm using your map in WPF, and I've noticed some peculiarities:

I've noticed that, when the ZoomToBounds assigns to TargetZoomLevel, the value is an un-rounded number. This makes perfect sense, as the calculation produces a number to fit the area precisely. However, when the map calls RemoveAnimation(DP dp), I noticed that the WPF plumbing appears to assign to ZoomLevel again, and it takes a guess on the final value should be for ZoomLevelProperty, and rounds the value. This causes the map to jump - the animation smoothly animates to the bounds, and then suddenly jumps to a whole-number zoom level, typically higher rather than lower than the animated value. This causes the map to trim the edges of the defined boundary given to the map, meaning some items that are, hypothetically, on the edge of that defined boundary will no longer be visible.

I have fixed the aforementioned issue for myself by truncating the value to a lesser whole number before setting TargetZoomLevel, i.e.
            TargetZoomLevel = Math.Floor(Math.Min(lonZoom, latZoom));
inside of ZoomToBounds. This appears to allow WPF to keep the final value of animation, rather than assigning a new, rounded value to ZoomLevel when the animation is removed.

I might be mis-diagnosing the problem, so it may be worthwhile to spend some time looking at the WPF code using a decompiler such as .NET Reflector to see for yourself, but I thought I'd bring this issue to your attention, should you care to use my solution.

Kind Regards,
PisoMojado
Coordinator
Jun 9, 2015 at 8:07 AM
No idea what you mean. I am unable to reproduce this behaviour, and I seriously doubt that there is anything in WPF that rounds the value of a double dependency property.
Jul 17, 2015 at 7:45 PM
I just wanted to post an update. Apologies to ClemensF: you were right of course, the dependency property back-end does not just elect to round floating point values when it feels like it.

However, I have discovered the source of my bug, and thought I might share in case others are interested: If a dependency property is of a floating point type, and it is bound to a discrete numeric property - be it in the UI or the datacontext - it looks as though the system automatically coerces the value by rounding it. This does seem to be driven by the machinery not in our control (i.e. the DependencyProperty back-end), which is why it appeared that way.

When this happens to a property that is being animated - the way zoom in the map control is animated using TargetZoom - the animation will end, and the coerced value inside the integer property will propagate back to the Map, and the Zoom will "jump" to the rounded value.