diff options
| author | Justin Bronn <jbronn@gmail.com> | 2007-09-12 13:04:33 +0000 |
|---|---|---|
| committer | Justin Bronn <jbronn@gmail.com> | 2007-09-12 13:04:33 +0000 |
| commit | b74f5ef66f019eaf5e2d4ce206064b30c41f6a75 (patch) | |
| tree | b600194248939ec2c4f26ded2398fad64ded5c12 | |
| parent | ceac9c9645e12d426edd6cb2e5c11f2cb5048337 (diff) | |
gis: added the beginnings of a `maps` module, for generating mapping framework JavaScript; cleaned up the sitemaps module and KML templates; added shortcuts module.
git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@6108 bcc190cf-cafb-0310-a4f2-bffc1f526a37
| -rw-r--r-- | django/contrib/gis/maps/__init__.py | 0 | ||||
| -rw-r--r-- | django/contrib/gis/maps/google/__init__.py | 60 | ||||
| -rw-r--r-- | django/contrib/gis/maps/google/gmap.py | 100 | ||||
| -rw-r--r-- | django/contrib/gis/maps/google/zoom.py | 164 | ||||
| -rw-r--r-- | django/contrib/gis/maps/openlayers/__init__.py | 0 | ||||
| -rw-r--r-- | django/contrib/gis/shortcuts.py | 12 | ||||
| -rw-r--r-- | django/contrib/gis/sitemaps.py | 19 | ||||
| -rw-r--r-- | django/contrib/gis/templates/gis/google/js/google-map.js | 16 | ||||
| -rw-r--r-- | django/contrib/gis/templates/gis/kml.xml | 10 | ||||
| -rw-r--r-- | django/contrib/gis/templates/gis/kml/base.kml | 6 | ||||
| -rw-r--r-- | django/contrib/gis/templates/gis/kml/placemarks.kml | 8 |
11 files changed, 372 insertions, 23 deletions
diff --git a/django/contrib/gis/maps/__init__.py b/django/contrib/gis/maps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/contrib/gis/maps/__init__.py diff --git a/django/contrib/gis/maps/google/__init__.py b/django/contrib/gis/maps/google/__init__.py new file mode 100644 index 0000000000..ec3d372453 --- /dev/null +++ b/django/contrib/gis/maps/google/__init__.py @@ -0,0 +1,60 @@ +""" + This module houses the GoogleMap object, used for generating + the needed javascript to embed Google Maps in a webpage. + + Google(R) is a registered trademark of Google, Inc. of Mountain View, California. + + Example: + + * In the view: + return render_to_response('template.html', {'google' : GoogleMap(key="abcdefg")}) + + * In the template: + + <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + {{ google.xhtml }} + <head> + <title>Google Maps via GeoDjango</title> + {{ google.style }} + {{ google.scripts }} + </head> + {{ google.body }} + <div id="{{ google.dom_id }}"></div> + </body> + </html> + + Note: If you want to be more explicit in your templates, the following are + equivalent: + {{ google.body }} => "<body {{ google.onload }} {{ google.onunload }}>" + {{ google.xhtml }} => "<html xmlns="http://www.w3.org/1999/xhtml" {{ google.xmlns }}>" + {{ google.style }} => "<style>{{ google.vml_css }}</style>" + + Explanation: + - The `xhtml` property provides the correct XML namespace needed for + Google Maps to operate in IE using XHTML. Google Maps on IE uses + VML to draw polylines. Returns, by default: + <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"> + + - The `style` property provides the correct style tag for the CSS + properties required by Google Maps on IE: + <style type="text/css">v\:* {behavior:url(#default#VML);}</style> + + - The `scripts` property provides the necessary <script> tags for + including the Google Maps javascript, as well as including the + generated javascript. + + - The `body` property provides the correct attributes for the + body tag to load the generated javascript. By default, returns: + <body onload="gmap_load()" onunload="GUnload()"> + + - The `id` property returns the DOM id for the map. Defaults to "map". + + The following attributes may be set or customized in your local settings: + * GOOGLE_MAPS_API_KEY: String of your Google Maps API key. These are tied to + to a domain. May be obtained from http://www.google.com/apis/maps/ + * GOOGLE_MAPS_API_VERSION (optional): Defaults to using "2.x" + * GOOGLE_MAPS_URL (optional): Must have a substitution ('%s') for the API + version. +""" +from django.contrib.gis.maps.google.gmap import GoogleMap +from django.contrib.gis.maps.google.zoom import GoogleZoom diff --git a/django/contrib/gis/maps/google/gmap.py b/django/contrib/gis/maps/google/gmap.py new file mode 100644 index 0000000000..805dc25af4 --- /dev/null +++ b/django/contrib/gis/maps/google/gmap.py @@ -0,0 +1,100 @@ +from django.conf import settings +from django.template.loader import render_to_string + +# The default Google Maps URL (for the API javascript) +# TODO: Internationalize for Japan, UK, etc. +GOOGLE_MAPS_URL='http://maps.google.com/maps?file=api&v=%s&key=' + +class GoogleMapException(Exception): pass + +class GoogleMap(object): + "A class for generating Google Maps javascript." + + # String constants + onunload = 'onunload="GUnload()"' # Cleans up after Google Maps + vml_css = 'v\:* {behavior:url(#default#VML);}' # CSS for IE VML + xmlns = 'xmlns:v="urn:schemas-microsoft-com:vml"' # XML Namespace (for IE VML). + + def __init__(self, key=None, api_url=None, version=None, + center_lat=0.0, center_lon=0.0, zoom=1, + dom_id='map', load_func='gmap_load', + kml_urls=[], template='gis/google/js/google-map.js'): + + # The Google Maps API Key defined in the settings will be used + # if not passed in as a parameter. The use of an API key is + # _required_. + if not key: + try: + self._key = settings.GOOGLE_MAPS_API_KEY + except AttributeError: + raise GoogleMapException, 'Google Maps API Key not found (try adding GOOGLE_MAPS_API_KEY to your settings).' + else: + self._key = key + + # Getting the Google Maps API version, defaults to using the latest ("2.x"), + # this is not necessarily the most stable. + if not version: + try: + self._version = settings.GOOGLE_MAPS_API_VERSION + except AttributeError: + self._version = '2.x' + else: + self._version = version + + # Can specify the API URL in the `api_url` keyword. + if not api_url: + try: + self._url = settings.GOOGLE_MAPS_URL % self._version + except AttributeError: + self._url = GOOGLE_MAPS_URL % self._version + else: + self._url = api_url + + # Setting the DOM id of the map, the center lat/lon, the load function, + # and the zoom. + self.dom_id = dom_id + self.center_lat = center_lat + self.center_lon = center_lon + self.load_func = load_func + self.template = template + self.zoom = zoom + + # Setting the parameters for the javascript template. + params = {'center_lat' : center_lat, + 'center_lon' : center_lon, + 'dom_id' : dom_id, + 'kml_urls' : kml_urls, + 'load_func' : load_func, + 'zoom' : zoom, + } + self.js = render_to_string(template, params) + + @property + def body(self): + "Returns HTML body tag for loading and unloading Google Maps javascript." + return '<body %s %s>' % (self.onload, self.onunload) + + @property + def onload(self): + "Returns the `onload` HTML <body> attribute." + return 'onload="%s()"' % self.load_func + + @property + def api_script(self): + "Returns the <script> tag for the Google Maps API javascript." + return '<script src="%s%s" type="text/javascript"></script>' % (self._url, self._key) + + @property + def scripts(self): + "Returns all <script></script> tags required for Google Maps JavaScript." + return '%s\n <script type="text/javascript">\n//<![CDATA[\n%s//]]>\n </script>' % (self.api_script, self.js) + + @property + def style(self): + "Returns additional CSS styling needed for Google Maps on IE." + return '<style type="text/css">%s</style>' % self.vml_css + + @property + def xhtml(self): + "Returns XHTML information needed for IE VML overlays." + return '<html xmlns="http://www.w3.org/1999/xhtml" %s>' % self.xmlns diff --git a/django/contrib/gis/maps/google/zoom.py b/django/contrib/gis/maps/google/zoom.py new file mode 100644 index 0000000000..16b18ac555 --- /dev/null +++ b/django/contrib/gis/maps/google/zoom.py @@ -0,0 +1,164 @@ +from django.contrib.gis.geos import GEOSGeometry, LinearRing, Polygon, Point +from django.contrib.gis.maps.google.gmap import GoogleMapException +from math import pi, sin, cos, log, exp, atan + +# Constants used for degree to radian conversion, and vice-versa. +DTOR = pi / 180. +RTOD = 180. / pi + +def get_width_height(envelope): + # Getting the lower-left, upper-left, and upper-right + # coordinates of the envelope. + ll = Point(envelope[0][0]) + ul = Point(envelope[0][1]) + ur = Point(envelope[0][2]) + + height = ll.distance(ul) + width = ul.distance(ur) + return width, height + +class GoogleZoom(object): + """ + GoogleZoom is a utility for performing operations related to the zoom + levels on Google Maps. + + This class is inspired by the OpenStreetMap Mapnik tile generation routine + `generate_tiles.py`, and the article "How Big Is the World" (Hack #16) in + "Google Maps Hacks" by Rich Gibson and Schuyler Erle. + + `generate_tiles.py` may be found at: + http://trac.openstreetmap.org/browser/applications/rendering/mapnik/generate_tiles.py + + "Google Maps Hacks" may be found at http://safari.oreilly.com/0596101619 + """ + + def __init__(self, num_zoom=19, tilesize=256): + "Initializes the Google Zoom object." + + # Google's tilesize is 256x256, square tiles are assumed. + self._tilesize = tilesize + + # The number of zoom levels + self._nzoom = num_zoom + + # Initializing arrays to hold the parameters for each + # one of the zoom levels. + self._degpp = [] # Degrees per pixel + self._radpp = [] # Radians per pixel + self._npix = [] # 1/2 the number of pixels for a tile at the given zoom level + + # Incrementing through the zoom levels and populating the + # parameter arrays. + z = tilesize # The number of pixels per zoom level. + for i in xrange(num_zoom): + # Getting the degrees and radians per pixel, and the 1/2 the number of + # for every zoom level. + self._degpp.append(z / 360.) # degrees per pixel + self._radpp.append(z / (2 * pi)) # radians per pixl + self._npix.append(z / 2) # number of pixels to center of tile + + # Multiplying `z` by 2 for the next iteration. + z *= 2 + + def __len__(self): + "Returns the number of zoom levels." + return self._nzoom + + def get_lon_lat(self, lonlat): + "Unpacks longitude, latitude from GEOS Points and 2-tuples." + if isinstance(lonlat, Point): + lon, lat = lonlat.coords + else: + lon, lat = lonlat + return lon, lat + + def lonlat_to_pixel(self, lonlat, zoom): + "Converts a longitude, latitude coordinate pair for the given zoom level." + # Setting up, unpacking the longitude, latitude values and getting the + # number of pixels for the given zoom level. + lon, lat = self.get_lon_lat(lonlat) + npix = self._npix[zoom] + + # Calculating the pixel x coordinate by multiplying the longitude + # value with with the number of degrees/pixel at the given + # zoom level. + px_x = round(npix + (lon * self._degpp[zoom])) + + # Creating the factor, and ensuring that 1 or -1 is not passed in as the + # base to the logarithm. Here's why: + # if fac = -1, we'll get log(0) which is undefined; + # if fac = 1, our logarithm base will be divided by 0, also undefined. + fac = min(max(sin(DTOR * lat), -0.9999), 0.9999) + + # Calculating the pixel y coordinate. + px_y = round(npix + (0.5 * log((1 + fac)/(1 - fac)) * (-1.0 * self._radpp[zoom]))) + + # Returning the pixel x, y to the caller of the function. + return (px_x, px_y) + + def pixel_to_lonlat(self, px, zoom): + "Converts a pixel to a longitude, latitude pair at the given zoom level." + if len(px) != 2: + raise TypeError, 'Pixel should be a sequence of two elements.' + + # Getting the number of pixels for the given zoom level. + npix = self._npix[zoom] + + # Calculating the longitude value, using the degrees per pixel. + lon = (px[0] - npix) / self._degpp[zoom] + + # Calculating the latitude value. + lat = RTOD * ( 2 * atan(exp((px[1] - npix)/ (-1.0 * self._radpp[zoom]))) - 0.5 * pi) + + # Returning the longitude, latitude coordinate pair. + return (lon, lat) + + def tile(self, lonlat, zoom): + """ + Returns a Polygon corresponding to the region represented by a fictional + Google Tile for the given longitude/latitude pair and zoom level. This + tile is used to determine the size of a tile at the given point. + """ + # The given lonlat is the center of the tile. + delta = self._tilesize / 2 + + # Getting the pixel coordinates corresponding to the + # the longitude/latitude. + px = self.lonlat_to_pixel(lonlat, zoom) + + # Getting the lower-left and upper-right lat/lon coordinates + # for the bounding box of the tile. + ll = self.pixel_to_lonlat((px[0]-delta, px[1]-delta), zoom) + ur = self.pixel_to_lonlat((px[0]+delta, px[1]+delta), zoom) + + # Constructing the Polygon, representing the tile and returning. + return Polygon(LinearRing(ll, (ll[0], ur[1]), ur, (ur[0], ll[1]), ll), srid=4326) + + def get_zoom(self, geom): + "Returns the optimal Zoom level for the given geometry." + + # Checking the input type. + if not isinstance(geom, GEOSGeometry) and geom.srid == 4326: + raise TypeError, 'get_zoom() expects a GEOS Geometry with an SRID of 4326.' + + # Getting the envelope for the geometry, and its associated width, height + # and centroid. + env = geom.envelope + env_w, env_h = get_width_height(env) + center = env.centroid + + for z in xrange(self._nzoom): + # Getting the tile at the zoom level. + tile = self.tile(center, z) + tile_w, tile_h = get_width_height(tile) + + # When we span more than one tile, this is an approximately good + # zoom level. + if (env_w > tile_w) or (env_h > tile_h): + if z == 0: + raise GoogleMapException, \ + 'Geometry width and height should not exceed that of the Earth.' + return z-1 + + # Otherwise, we've zoomed in to the max. + return self._nzoom-1 diff --git a/django/contrib/gis/maps/openlayers/__init__.py b/django/contrib/gis/maps/openlayers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/contrib/gis/maps/openlayers/__init__.py diff --git a/django/contrib/gis/shortcuts.py b/django/contrib/gis/shortcuts.py new file mode 100644 index 0000000000..8eeaed1aaa --- /dev/null +++ b/django/contrib/gis/shortcuts.py @@ -0,0 +1,12 @@ +from django.http import HttpResponse +from django.template import loader + +def render_to_kml(*args, **kwargs): + "Renders the response using the MIME type for KML." + return HttpResponse(loader.render_to_string(*args, **kwargs), + mimetype='application/vnd.google-earth.kml+xml kml') + +def render_to_text(*args, **kwargs): + "Renders the response using the MIME type for plain text." + return HttpResponse(loader.render_to_string(*args, **kwargs), + mimetype='text/plain') diff --git a/django/contrib/gis/sitemaps.py b/django/contrib/gis/sitemaps.py index 1ce0493472..d5d11b9c0f 100644 --- a/django/contrib/gis/sitemaps.py +++ b/django/contrib/gis/sitemaps.py @@ -1,18 +1,13 @@ from django.core import urlresolvers from django.contrib.sitemaps import Sitemap from django.contrib.gis.db.models.fields import GeometryField +from django.contrib.gis.shortcuts import render_to_kml from django.db.models import get_model, get_models -from django.template.loader import get_template -from django.template import Context from django.http import HttpResponse -kml_files = [('gis.school', 'location'), - ('gis.district', 'boundary')] - - class KMLSitemap(Sitemap): """ - A minimal hook to + A minimal hook to produce KML sitemaps. """ def __init__(self, locations=None): if locations is None: @@ -28,7 +23,6 @@ class KMLSitemap(Sitemap): kwargs={'label':obj[0], 'field_name':obj[1]}) - def _build_kml_sources(): "Make a mapping of all available KML sources." ret = [] @@ -45,18 +39,17 @@ def _build_kml_sources(): class KMLNotFound(Exception): pass - def kml(request, label, field_name): placemarks = [] klass = get_model(*label.split('.')) if not klass: raise KMLNotFound("You must supply a valid app.model label. Got %s" % label) + #FIXME: GMaps apparently has a limit on size of displayed kml files # check if paginating w/ external refs (i.e. linked list) helps. placemarks.extend(list(klass._default_manager.kml(field_name)[:100])) + #FIXME: other KML features? - t = get_template('gis/kml.xml') - c = Context({'places':placemarks}) - - return HttpResponse(t.render(c), mimetype='application/vnd.google-earth.kml+xml') + return render_to_kml('gis/kml/placemarks.kml', {'places' : placemarks}) + diff --git a/django/contrib/gis/templates/gis/google/js/google-map.js b/django/contrib/gis/templates/gis/google/js/google-map.js new file mode 100644 index 0000000000..cb78acf5a8 --- /dev/null +++ b/django/contrib/gis/templates/gis/google/js/google-map.js @@ -0,0 +1,16 @@ +{% block vars %}var map;{% for kml_url in kml_urls %}var kml{{ forloop.counter }};{% endfor %}{% endblock %} +{% block functions %}{% endblock %} +{% block load %}function {{ load_func }}(){ + if (GBrowserIsCompatible()) { + map = new GMap2(document.getElementById("{{ dom_id }}")); + map.addControl(new GSmallMapControl()); + map.addControl(new GMapTypeControl()); + map.setCenter(new GLatLng({{ center_lat }}, {{ center_lon }}), {{ zoom }}); + {% for kml_url in kml_urls %}kml{{ forloop.counter }} = new GGeoXml("{{ kml_url }}"); + map.addOverlay(kml{{ forloop.counter }});{% endfor %} + {% block load_extra %}{% endblock %} + }else { + alert("Sorry, the Google Maps API is not compatible with this browser."); + } +} +{% endblock %}
\ No newline at end of file diff --git a/django/contrib/gis/templates/gis/kml.xml b/django/contrib/gis/templates/gis/kml.xml deleted file mode 100644 index 7d121c2aa1..0000000000 --- a/django/contrib/gis/templates/gis/kml.xml +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> - <kml xmlns="http://earth.google.com/kml/2.1"> - {% for place in places %} - <Placemark> - <name>{{ place.name|escape }}</name> - <description>{{ place.description|escape }}</description> - {{ place.kml }} - </Placemark> - {% endfor %} -</kml> diff --git a/django/contrib/gis/templates/gis/kml/base.kml b/django/contrib/gis/templates/gis/kml/base.kml new file mode 100644 index 0000000000..374404c39f --- /dev/null +++ b/django/contrib/gis/templates/gis/kml/base.kml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<kml xmlns="http://earth.google.com/kml/{% block kml_version %}2.1{% endblock %}"> +<Document>{% block name %}{% endblock %} +{% block placemarks %}{% endblock %} +</Document> +</kml> diff --git a/django/contrib/gis/templates/gis/kml/placemarks.kml b/django/contrib/gis/templates/gis/kml/placemarks.kml new file mode 100644 index 0000000000..2b9b5e2f02 --- /dev/null +++ b/django/contrib/gis/templates/gis/kml/placemarks.kml @@ -0,0 +1,8 @@ +{% extends "gis/kml/base.kml" %} +{% block placemarks %}{% for place in places %} + <Placemark> + <name>{{ place.name|escape }}</name> + <description>{{ place.description|escape }}</description> + {{ place.kml }} + </Placemark>{% endfor %}{% endblock %} + |
