app/django/contrib/gis/maps/google/zoom.py
changeset 323 ff1a9aa48cfd
equal deleted inserted replaced
322:6641e941ef1e 323:ff1a9aa48cfd
       
     1 from django.contrib.gis.geos import GEOSGeometry, LinearRing, Polygon, Point
       
     2 from django.contrib.gis.maps.google.gmap import GoogleMapException
       
     3 from math import pi, sin, cos, log, exp, atan
       
     4 
       
     5 # Constants used for degree to radian conversion, and vice-versa.
       
     6 DTOR = pi / 180.
       
     7 RTOD = 180. / pi
       
     8 
       
     9 class GoogleZoom(object):
       
    10     """
       
    11     GoogleZoom is a utility for performing operations related to the zoom
       
    12     levels on Google Maps.
       
    13 
       
    14     This class is inspired by the OpenStreetMap Mapnik tile generation routine
       
    15     `generate_tiles.py`, and the article "How Big Is the World" (Hack #16) in
       
    16     "Google Maps Hacks" by Rich Gibson and Schuyler Erle.
       
    17 
       
    18     `generate_tiles.py` may be found at:
       
    19       http://trac.openstreetmap.org/browser/applications/rendering/mapnik/generate_tiles.py
       
    20 
       
    21     "Google Maps Hacks" may be found at http://safari.oreilly.com/0596101619
       
    22     """
       
    23     
       
    24     def __init__(self, num_zoom=19, tilesize=256):
       
    25         "Initializes the Google Zoom object."
       
    26         # Google's tilesize is 256x256, square tiles are assumed.
       
    27         self._tilesize = tilesize
       
    28         
       
    29         # The number of zoom levels
       
    30         self._nzoom = num_zoom
       
    31 
       
    32         # Initializing arrays to hold the parameters for each one of the 
       
    33         # zoom levels.
       
    34         self._degpp = [] # Degrees per pixel
       
    35         self._radpp = [] # Radians per pixel
       
    36         self._npix  = [] # 1/2 the number of pixels for a tile at the given zoom level
       
    37         
       
    38         # Incrementing through the zoom levels and populating the parameter arrays.
       
    39         z = tilesize # The number of pixels per zoom level.
       
    40         for i in xrange(num_zoom):
       
    41             # Getting the degrees and radians per pixel, and the 1/2 the number of
       
    42             # for every zoom level.
       
    43             self._degpp.append(z / 360.) # degrees per pixel
       
    44             self._radpp.append(z / (2 * pi)) # radians per pixl
       
    45             self._npix.append(z / 2) # number of pixels to center of tile
       
    46 
       
    47             # Multiplying `z` by 2 for the next iteration.
       
    48             z *= 2
       
    49 
       
    50     def __len__(self):
       
    51         "Returns the number of zoom levels."
       
    52         return self._nzoom
       
    53 
       
    54     def get_lon_lat(self, lonlat):
       
    55         "Unpacks longitude, latitude from GEOS Points and 2-tuples."
       
    56         if isinstance(lonlat, Point):
       
    57             lon, lat = lonlat.coords
       
    58         else:
       
    59             lon, lat = lonlat
       
    60         return lon, lat
       
    61 
       
    62     def lonlat_to_pixel(self, lonlat, zoom):
       
    63         "Converts a longitude, latitude coordinate pair for the given zoom level."
       
    64         # Setting up, unpacking the longitude, latitude values and getting the
       
    65         # number of pixels for the given zoom level.
       
    66         lon, lat = self.get_lon_lat(lonlat)
       
    67         npix = self._npix[zoom]
       
    68 
       
    69         # Calculating the pixel x coordinate by multiplying the longitude value
       
    70         # with with the number of degrees/pixel at the given zoom level.
       
    71         px_x = round(npix + (lon * self._degpp[zoom]))
       
    72 
       
    73         # Creating the factor, and ensuring that 1 or -1 is not passed in as the 
       
    74         # base to the logarithm.  Here's why:
       
    75         #  if fac = -1, we'll get log(0) which is undefined; 
       
    76         #  if fac =  1, our logarithm base will be divided by 0, also undefined.
       
    77         fac = min(max(sin(DTOR * lat), -0.9999), 0.9999)
       
    78 
       
    79         # Calculating the pixel y coordinate.
       
    80         px_y = round(npix + (0.5 * log((1 + fac)/(1 - fac)) * (-1.0 * self._radpp[zoom])))
       
    81 
       
    82         # Returning the pixel x, y to the caller of the function.
       
    83         return (px_x, px_y)
       
    84 
       
    85     def pixel_to_lonlat(self, px, zoom):
       
    86         "Converts a pixel to a longitude, latitude pair at the given zoom level."
       
    87         if len(px) != 2:
       
    88             raise TypeError('Pixel should be a sequence of two elements.')
       
    89 
       
    90         # Getting the number of pixels for the given zoom level.
       
    91         npix = self._npix[zoom]
       
    92 
       
    93         # Calculating the longitude value, using the degrees per pixel.
       
    94         lon = (px[0] - npix) / self._degpp[zoom]
       
    95 
       
    96         # Calculating the latitude value.
       
    97         lat = RTOD * ( 2 * atan(exp((px[1] - npix)/ (-1.0 * self._radpp[zoom]))) - 0.5 * pi)
       
    98 
       
    99         # Returning the longitude, latitude coordinate pair.
       
   100         return (lon, lat)
       
   101     
       
   102     def tile(self, lonlat, zoom):
       
   103         """
       
   104         Returns a Polygon  corresponding to the region represented by a fictional
       
   105         Google Tile for the given longitude/latitude pair and zoom level. This
       
   106         tile is used to determine the size of a tile at the given point.
       
   107         """
       
   108         # The given lonlat is the center of the tile.
       
   109         delta = self._tilesize / 2
       
   110 
       
   111         # Getting the pixel coordinates corresponding to the
       
   112         # the longitude/latitude.
       
   113         px = self.lonlat_to_pixel(lonlat, zoom)
       
   114 
       
   115         # Getting the lower-left and upper-right lat/lon coordinates
       
   116         # for the bounding box of the tile.
       
   117         ll = self.pixel_to_lonlat((px[0]-delta, px[1]-delta), zoom)
       
   118         ur = self.pixel_to_lonlat((px[0]+delta, px[1]+delta), zoom)
       
   119 
       
   120         # Constructing the Polygon, representing the tile and returning.
       
   121         return Polygon(LinearRing(ll, (ll[0], ur[1]), ur, (ur[0], ll[1]), ll), srid=4326)
       
   122         
       
   123     def get_zoom(self, geom):
       
   124         "Returns the optimal Zoom level for the given geometry."
       
   125         # Checking the input type.
       
   126         if not isinstance(geom, GEOSGeometry) or geom.srid != 4326:
       
   127             raise TypeError('get_zoom() expects a GEOS Geometry with an SRID of 4326.')
       
   128 
       
   129         # Getting the envelope for the geometry, and its associated width, height
       
   130         # and centroid.
       
   131         env = geom.envelope
       
   132         env_w, env_h = self.get_width_height(env.extent)
       
   133         center = env.centroid
       
   134 
       
   135         for z in xrange(self._nzoom):
       
   136             # Getting the tile at the zoom level.
       
   137             tile_w, tile_h = self.get_width_height(self.tile(center, z).extent)
       
   138 
       
   139             # When we span more than one tile, this is an approximately good
       
   140             # zoom level.
       
   141             if (env_w > tile_w) or (env_h > tile_h):
       
   142                 if z == 0: 
       
   143                     raise GoogleMapException('Geometry width and height should not exceed that of the Earth.')
       
   144                 return z-1
       
   145         
       
   146         # Otherwise, we've zoomed in to the max.
       
   147         return self._nzoom-1
       
   148 
       
   149     def get_width_height(self, extent):
       
   150         """
       
   151         Returns the width and height for the given extent.
       
   152         """
       
   153         # Getting the lower-left, upper-left, and upper-right
       
   154         # coordinates from the extent.
       
   155         ll = Point(extent[:2])
       
   156         ul = Point(extent[0], extent[3])
       
   157         ur = Point(extent[2:])
       
   158         # Calculating the width and height.
       
   159         height = ll.distance(ul)
       
   160         width  = ul.distance(ur)
       
   161         return width, height