Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic google map with custom tiles prevent repeating pan

Tags:

google-maps

I have a dynamic tile set where I do NOT want to allow panning outside of its bounds.

The below code gets me close, but the user can still scroll horizontally outside of strict bounds because it uses the map center for comparison

var strictBounds = new google.maps.LatLngBounds(
 new google.maps.LatLng(sw_lat, sw_lon), 
 new google.maps.LatLng(ne_lat, ne_lon)
);

google.maps.event.addListener(map, 'drag', function() 
{
  if (strictBounds.contains(map.getCenter())) return;

 // We're out of bounds - Move the map back within the bounds

 var c = map.getCenter(),
     x = c.lng(),
     y = c.lat(),
     maxX = strictBounds.getNorthEast().lng(),
     maxY = strictBounds.getNorthEast().lat(),
     minX = strictBounds.getSouthWest().lng(),
     minY = strictBounds.getSouthWest().lat();

 if (x < minX) x = minX;
 if (x > maxX) x = maxX;
 if (y < minY) y = minY;
 if (y > maxY) y = maxY;

 map.setCenter(new google.maps.LatLng(y, x));
});

What is happening

What I want

like image 834
Chris Muench Avatar asked Oct 10 '12 17:10

Chris Muench


1 Answers

For quick access, here is the jsfiddle for this solution: http://jsfiddle.net/nYz6k/


Supposed you're happy with the current solution of detecting bounds against the center, all you have to do is to restrict the bounds based on the current map canvas size.

The result that you're getting now is that every corner of the restricting bounds appears on the center of the map when it's full restricting (i.e. both latitude and longitude exceed the bounds corners).

The solution is to actually remove the x and y offset from the bounds, and still be able to check against the center (which is the most efficient solution, compared to other bounds-based checking solutions).

Also, you have to restrict the bounds only when you initialize the map and when the window is resizing, which means that when you pan there is no extra processing overhead besides the check method that you have already provided.

Don't forget to set the maxZoom property for the map (tweak it as needed) because above a determined zoom level the restricting bounds will by themselves fit in the viewport and there's no solution for that.

Important! Use 'center_changed' instead of 'drag' because 'drag' has that sliding behavior that when you finished dragging and set the center, the maps still slides in the direction of the panning.


Before you implement the solution I recommend you to check out how the Maps API coordinate system and projection works, because this solution is heavily based on it, and if you want to tweak it out, it's useful to know this information.

https://developers.google.com/maps/documentation/javascript/maptypes and check out the Custom Map Types -> Map Coordinates section.


Here's what you need to do. First, you need to implement 2 short methods of converting between geographic coordinates (LatLng) and pixel coordinates.

var fromLatLngToPixel = function (latLng) {
  var point = map.getProjection().fromLatLngToPoint(latLng);
  var zoom = map.getZoom();
  return new google.maps.Point(
    Math.floor(point.x * Math.pow(2, zoom)),
    Math.floor(point.y * Math.pow(2, zoom))
  );
}

var fromPixelToLatLng = function (pixel) {
  var zoom = map.getZoom();
  var point = new google.maps.Point(
    pixel.x / Math.pow(2, zoom),
    pixel.y / Math.pow(2, zoom)
  );
  return map.getProjection().fromPointToLatLng(point);
}

Next, implement the method that effectively restricts the bounds. Note that you always have to keep a variable that stores the original bounds, because each time the map canvas is resized, the resulting bounds will change.

For this, let's say originalBounds keeps the bounds you provided with sw_lat, sw_long etc, and shrinkedBounds is modified by this method. You can rename it to strictBounds to still work in your method, but it's up to you. This uses jQuery for getting the width and height of the canvas object.

var shrinkBounds = function () {
  zoom = map.getZoom();

  // The x and y offset will always be half the current map canvas
  // width and height respectively
  xoffset = $('#map_canvas').width() / 2;
  yoffset = $('#map_canvas').height() / 2;

  // Convert the bounds extremities to global pixel coordinates
  var pixswOriginal = fromLatLngToPixel(originalBounds.getSouthWest());
  var pixneOriginal = fromLatLngToPixel(originalBounds.getNorthEast());

  // Shrink the original bounds with the x and y offset
  var pixswShrinked = new google.maps.Point(pixswOriginal.x + xoffset, pixswOriginal.y - yoffset);
  var pixneShrinked = new google.maps.Point(pixneOriginal.x - xoffset, pixneOriginal.y + yoffset);

  // Rebuild the shrinkedBounds object with the modified 
  shrinkedBounds = new google.maps.LatLngBounds(
    fromPixelToLatLng(pixswShrinked),
    fromPixelToLatLng(pixneShrinked));
}

Next, all you have to do is to call this method:

  • once when the map is initialized. Be aware that depending on when you call the method, you may be getting some weird errors because the map might not have yet initialized all it's properties. Best way is to use the projection_changed event.

    google.maps.event.addListener(map, 'projection_changed', function (e) {
      shrinkBounds();
    });
    
  • every time the map canvas is resized

     google.maps.event.addListener(map, 'resize', function (e) {
       shrinkBounds();
     });
    

But the most important part of this is that the 'resize' event is never triggered for programmatic resizes of the map container, so you have to trigger it manually every time you resize the canvas programmatically (if you do, but I doubt you do).

The most common way is to trigger it when the window is resized:

$(window).resize(function () {
  google.maps.event.trigger(map, 'resize');
});

After all of this, you can now safely use your method, but as a handler for 'center_changed' and not for 'drag' as I've mentioned earlier.


I set up a jsfiddle with the full working code.

http://jsfiddle.net/nYz6k/

You can see the restricting at the bottom left corner with that little half-marker that shows up, which is positioned in the SW corner of the bounds. There is also one at the NW corner but naturally you can't see it because it's displayed above the position.

like image 181
Tiborg Avatar answered Sep 22 '22 10:09

Tiborg