none
Ajax7 Map SetView not working with LocationRect that overlaps the international date line RRS feed

  • Question

  • Hi, my maps playing up when I try to setView on polygons of Russia, Fiji, New Zealand & The United States.

    Anyone else experience something similar?


    Jonathon
    • Moved by Ricky_Brundritt Friday, March 16, 2012 6:50 PM (From:Bing Maps: Map Control and Web services Development)
    Tuesday, February 1, 2011 10:11 PM

Answers

  • Yes, I can recreate this.

    When the LocationRect crosses the 180th meridian then map.SetView appears to set the map to the correct centrepoint, but resets to zoom level 1. This seems to happen however the LocationRect was created (fromLocations, fromEdges, fromCorners, or default constructor), so I guess it's a bug in map.SetView() - is that the same behaviour you're seeing?

    This is in either FF or IE under Win7.


    Beginning Spatial with SQL Server http://www.apress.com/book/view/1430218290
    Tuesday, February 1, 2011 10:56 PM
    Moderator
  • As a quick 'n' dirty proof, I added an event handler to my map:

    Microsoft.Maps.Events.addHandler(map, "dblclick", DoubleclickCallback);

    And, in the callback:

    function DoubleclickCallback(e) {
      e.handled = true;
      var Bounds = map.getBounds();
      map.setView({ bounds: Bounds });
    }

    Sure enough, whenever you double click on a map that doesn't have the 180th meridian in view, nothing happens (as you'd expect).

    If you click when the map does show the 180th meridian, it resets to zoom level 1.


    Beginning Spatial with SQL Server http://www.apress.com/book/view/1430218290
    Tuesday, February 1, 2011 11:01 PM
    Moderator
  • Hello tanoshimi - If you look at the thread, I posted a workaround to the problem.

    Friday, March 2, 2012 7:01 PM

All replies

  • Yes, I can recreate this.

    When the LocationRect crosses the 180th meridian then map.SetView appears to set the map to the correct centrepoint, but resets to zoom level 1. This seems to happen however the LocationRect was created (fromLocations, fromEdges, fromCorners, or default constructor), so I guess it's a bug in map.SetView() - is that the same behaviour you're seeing?

    This is in either FF or IE under Win7.


    Beginning Spatial with SQL Server http://www.apress.com/book/view/1430218290
    Tuesday, February 1, 2011 10:56 PM
    Moderator
  • As a quick 'n' dirty proof, I added an event handler to my map:

    Microsoft.Maps.Events.addHandler(map, "dblclick", DoubleclickCallback);

    And, in the callback:

    function DoubleclickCallback(e) {
      e.handled = true;
      var Bounds = map.getBounds();
      map.setView({ bounds: Bounds });
    }

    Sure enough, whenever you double click on a map that doesn't have the 180th meridian in view, nothing happens (as you'd expect).

    If you click when the map does show the 180th meridian, it resets to zoom level 1.


    Beginning Spatial with SQL Server http://www.apress.com/book/view/1430218290
    Tuesday, February 1, 2011 11:01 PM
    Moderator
  • I am having the same problem.  Any word on a resolution yet?
    Saturday, February 12, 2011 12:23 AM
  • This is known issue with V7 mapcontrol & would be fixed in upcoming updates to it. I will repond once the updates are available
    Tuesday, February 15, 2011 11:00 PM
  • Any word on resolution?

    It's been more than a year now.

    Using setView() with any bounding box that includes New Zealand, which spans the 180th meridian, causes the same problem. (the Chatham Islands are on the other side from the main landmass of NZ)

    The setting of zoom=1 seems to be a symptom.  The real issue is that the calculated bounding box never spans the 180th, and the only map that can display a box that wide must have zoom = 1.

    My guess at the cause of the problem: LocationRect.fromLocations() and LocationRect.fromCorners() are using a naive arithmetic approach to calculate the bounding box.  Thse methods need to set the northwest and southeast corners of the box not through latitude/longitude arithmetic, but by figuring the minimal great circle distances.

    Thursday, March 1, 2012 8:04 PM
  • Yes, I just looked at veapicore.js .  This is in version 7.0.20120123200232.91 , available at

    http://ecn.dev.virtualearth.net/mapcontrol/v7.0/js/bin/7.0.20120123200232.91/en-us/veapicore.js

    As I guessed, LocationRect.fromLocations uses a naive approach. It does not calculate a minimum bounding rectangle (MBR) from the set of locations. Instead it simply compares absolute values of longitudes and latitudes.  In effect, it models the world as a flat rectangle, without a 180th meridian. 

    tt.fromEdges = function(n, t, i, r, u, f) {
        return Math.abs(r - t) > 360 && (t = -180, r = 180), t = l.normalizeLongitude(t), r = l.normalizeLongitude(r), t > r && (r += 360), new tt(new l((i + n) / 2, (t + r) / 2, u, f), r - t, n - i)
      }, 
    
    tt.fromCorners = function(n, t) {
        return tt.fromEdges(n.latitude, n.longitude, t.latitude, t.longitude, n.altitude, n.altitudeReference)
      }, 
    
    tt.fromLocations = function() {
        var o = nr(arguments[0]) ? arguments[0] : arguments,
          r, u, t, i, s, e, n, f = o.length;
        while (f--) n = o[f], isFinite(n.latitude) && isFinite(n.longitude) && (r = r === c ? n.latitude : Math.max(r, n.latitude), u = u === c ? n.latitude : Math.min(u, n.latitude), i = i === c ? n.longitude : Math.max(i, n.longitude), t = t === c ? n.longitude : Math.min(t, n.longitude), isFinite(n.altitude) && pt.isValid(n.altitudeReference) && (e = n.altitude, s = n.altitudeReference));
        return tt.fromEdges(r, t, u, i, e, s)
      }, 

    Thursday, March 1, 2012 9:12 PM
  • In effect, it models the world as a flat rectangle, without a 180th meridian.

    I agree with your conclusion - this approach would be fine on a flat map with edges, but not on a seamless map that wraps around.


    twitter: @alastaira blog: http://alastaira.wordpress.com/

    Thursday, March 1, 2012 9:32 PM
    Moderator
  • More on this.

    Looking further at veapicore.js,  I found two distinct problems.

    • The MapMath.locationRectToMercatorZoom function (an internal function, not intended for direct use by applications) always returns a zoom of 1 when the bounding box of the LocationRect spans the 180th meridian. This is incorrect.  This function is used by the Map.setView() function to auto-set the zoom, which results in the zoom waaay out  phenomenon described earlier in this thread.
    • The LocationRect.fromLocations() uses a naive approach for determining the bounding box for a set of locations. In fact it is not guaranteed to be a "minimum bounding box" or "minimum bounding rectangle" to use the terms of art in GIS.  The returned box never spans the 180th meridian. For example, the LocationRect returned for a set of locations representing the borders of the islands of New Zealand, will start at the -176th meridian and stretch East, all the way to the +165th meridian. This is just wrong.

    I don't quite know how to fix the second problem.

    To correct the first problem, anyone can monkey patch the relevant function within your own app code. (To monkey patch means to replace the definition of the function at runtime. It's possible in dynamic late-bound environments like javascript).  Do it by calling this function:

      function monkeyPatchMapMath() {
        Microsoft.Maps.InternalNamespaceForDelay.MapMath.
          locationRectToMercatorZoom = function (windowDimensions, bounds) {
            var ins = Microsoft.Maps.InternalNamespaceForDelay,
              d = windowDimensions,
              g = Microsoft.Maps.Globals,
              n = bounds.getNorth(),
              s = bounds.getSouth(),
              e = bounds.getEast(),
              w = bounds.getWest(),
              f = ((e+360 - w) % 360)/360,
              //f = Math.abs(w - e) / 360,
              u = Math.abs(ins.MercatorCube.latitudeToY(n) -
                           ins.MercatorCube.latitudeToY(s)),
              r = Math.min(d.width / (g.zoomOriginWidth * f),
                           d.height / (g.zoomOriginWidth * u));
            return ins.VectorMath.log2(r);
          };
      }

    The locationRectToMercatorZoom fn is delay-loaded, so you cannot simply call that function at startup. You need to call the monkeyPatch() function after the original, broken implementation has been loaded. A good time to call it is right before you call map.setView().  Call it just once, though.

    Your app code might look like this:

        var nw = new MM.Location(top, left),
          ne = new MM.Location(top, right),
          se = new MM.Location(bottom, right),
          sw = new MM.Location(bottom, left),
        bounds = MM.LocationRect.fromCorners(nw,se);
    
        monkeyPatchMapMath();
        map1.setView({bounds:bounds});

    That assumes map1 is a Microsoft.Maps.Map that has previously been created.

    This example employs a bounding box determined by the top, left, right, bottom variables, quantities that are manually determined elsewhere (Not shown).  As I said earlier, you cannot use fromLocations() to compute those edges automatically, when the locations span the 180th meridian.




    • Edited by cheeso Friday, March 2, 2012 7:15 PM
    Friday, March 2, 2012 3:30 AM
  • ok last post on this.

    I figured a patch for LocationRect.fromLocations as well. It turns out that finding the minimum bounding box is computationally expensive. Rather than do an exhaustive job, this patch changes fromLocations() so that it employs a heuristic - a "trick".

    The basic approach in the official fromLocations is to iterate through all locations and take the minimum longitude to be the "left" and the maximum longitude to be the "right".

    This fails when the minimum longitude is just to the east of the 180th meridian (say, -178), and the max longitude value is just to the west of the same line (say, +165).  The resulting bounding box should span the 180th meridian but in fact the value calculated using this naive approach goes the long way around.

    The heuristic approach is to compute two minimum bounding boxes, and select the smaller of the two.

    The first is calcuated as above. The second is calculated in almost the same way, except rather than using the longitude value, it uses the longitude value + 360, when the longitude is negative. The resulting transform changes longitude from a value that ranges from -180 to 180, into a value that ranges from 0 to 360. And then the function computes the maximum and minimum of that new set of values.

    The result is two bounding boxes: one with longitudes that range from -180 to +180, and another with longitudes that range from 0 to 360.  These boxes will be of different widths.

    By calculating both and choosing the box with the narrower width, we're making a guess that the smaller box is the correct answer. This heuristic will break if you are trying to calculate the bounding box for a set of points that is larger than half the earth.

    Here's the code:

    function monkeyPatchFromLocations() { Microsoft.Maps.LocationRect.fromLocations = function () { var com = Microsoft.Maps.InternalNamespaceForDelay.Common, o = com.isArray(arguments[0]) ? arguments[0] : arguments, latMax, latMin, lngMin1, lngMin2, lngMax1, lngMax2, c, lngMin, lngMax, LL, dx1, dx2, pt = Microsoft.Maps.AltitudeReference, s, e, n, f = o.length; while (f--) n = o[f], isFinite(n.latitude) && isFinite(n.longitude) && (latMax = latMax === c ? n.latitude : Math.max(latMax, n.latitude), latMin = latMin === c ? n.latitude : Math.min(latMin, n.latitude), lngMax1 = lngMax1 === c ? n.longitude : Math.max(lngMax1, n.longitude), lngMin1 = lngMin1 === c ? n.longitude : Math.min(lngMin1, n.longitude), LL = n.longitude, (LL < 0) && (LL += 360), lngMax2 = lngMax2 === c ? LL : Math.max(lngMax2, LL), lngMin2 = lngMin2 === c ? LL : Math.min(lngMin2, LL), isFinite(n.altitude) && pt.isValid(n.altitudeReference) && (e = n.altitude, s = n.altitudeReference)); dx1 = lngMax1 - lngMin1, dx2 = lngMax2 - lngMin2, lngMax = (dx1 > dx2) ? lngMax2 : lngMax1, lngMin = (dx1 > dx2) ? lngMin2 : lngMin1; return MM.LocationRect.fromEdges(latMax, lngMin, latMin, lngMax, e, s); }; }

    And as above, you must call this once, to fixup the fromLocations() method. Example usage:

        monkeyPatchFromLocations();
        bounds = Microsoft.Maps.LocationRect.fromLocations(allPoints);
        monkeyPatchMapMath();
        map1.setView({bounds:bounds});

    This works for me.


    • Edited by cheeso Friday, March 2, 2012 5:34 AM
    Friday, March 2, 2012 4:37 AM
  • This page demonstrates the problem as described above.

    http://jsbin.com/emobav/2

    Friday, March 2, 2012 6:11 AM
  • Hello tanoshimi - If you look at the thread, I posted a workaround to the problem.

    Friday, March 2, 2012 7:01 PM
  • Great - thanks for sharing, cheeso.

    twitter: @alastaira blog: http://alastaira.wordpress.com/

    Friday, March 2, 2012 7:34 PM
    Moderator
  • Hi Cheeso,

    Thanks for spending the time investigating and trying to workaround the problem. There's a fix for both issue in the next release, i.e setView not setting the correct zoom level and fromLocations not creating the minimum bounding box. Please check it out when the release goes out. More details at: http://social.msdn.microsoft.com/Forums/en-US/vemapcontroldev/thread/c481fe9c-a122-45be-ba92-38216c88499c

    A comment regarding your workarounds. You're using internal functions in your workaround, and this can change between releases. It is advisable to avoid it if possible. For example, in your fromLocations workaround. It would have been safer to have your own fromLocations function that returns a LocationRect, instead of overwriting the LocationRect.fromLocations.

    Thursday, March 8, 2012 6:22 PM
  • Hi, 

    I can still reproduce the 'shifting' issue on the latest release with a getZoom() value of 3 or lower. 

    Can anyone else still reproduce it?

    Thanks,

    Adam


    Friday, November 9, 2012 9:23 AM