From ae0e5835107ff6aa5fe5f4b44fadf318d843462a Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Mon, 10 Mar 2008 20:45:51 +0000 Subject: [PATCH] gis: Fixed #6746 by marking safe `GoogleMap` internal XHTML/JavaScript; added support for `GPolygon` and `GPolyline` overlays via the `polygons` and `polylines` keywords; the `zoom` keyword may now take a geometry for automatic zoom level determination; *.pyc files are now ignored in `django.contrib.gis` modules. git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7213 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/gis/maps/google/__init__.py | 7 +- django/contrib/gis/maps/google/gmap.py | 120 ++++++++++++------ django/contrib/gis/maps/google/overlays.py | 101 +++++++++++++++ django/contrib/gis/maps/google/zoom.py | 10 +- .../gis/templates/gis/google/js/google-map.js | 16 ++- 5 files changed, 200 insertions(+), 54 deletions(-) create mode 100644 django/contrib/gis/maps/google/overlays.py diff --git a/django/contrib/gis/maps/google/__init__.py b/django/contrib/gis/maps/google/__init__.py index ec3d372453..a2e38e4c90 100644 --- a/django/contrib/gis/maps/google/__init__.py +++ b/django/contrib/gis/maps/google/__init__.py @@ -19,7 +19,7 @@ {{ google.scripts }} {{ google.body }} -
+
@@ -47,7 +47,7 @@ body tag to load the generated javascript. By default, returns: - - The `id` property returns the DOM id for the map. Defaults to "map". + - The `dom_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 @@ -56,5 +56,6 @@ * 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.gmap import GoogleMap, GZOOM +from django.contrib.gis.maps.google.overlays import GPolygon, GPolyline 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 index 32713fc7b0..f1d3eaad2c 100644 --- a/django/contrib/gis/maps/google/gmap.py +++ b/django/contrib/gis/maps/google/gmap.py @@ -1,24 +1,32 @@ from django.conf import settings +from django.contrib.gis import geos from django.template.loader import render_to_string +from django.utils.safestring import mark_safe + +# Declaring the GoogleMapException prior to getting the +# default `GZOOM` GoogleZoom instance. +class GoogleMapException(Exception): pass +from django.contrib.gis.maps.google.overlays import GPolygon, GPolyline +from django.contrib.gis.maps.google.zoom import GoogleZoom +GZOOM = GoogleZoom() # 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." + "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). + onunload = mark_safe('onunload="GUnload()"') # Cleans up after Google Maps + vml_css = mark_safe('v\:* {behavior:url(#default#VML);}') # CSS for IE VML + xmlns = mark_safe('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', + center=None, center_lat=0.0, center_lon=0.0, + zoom=None, dom_id='map', load_func='gmap_load', + kml_urls=[], polygons=[], polylines=[], + template='gis/google/js/google-map.js', extra_context={}): # The Google Maps API Key defined in the settings will be used @@ -26,77 +34,109 @@ class GoogleMap(object): # _required_. if not key: try: - self._key = settings.GOOGLE_MAPS_API_KEY + 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).' + raise GoogleMapException('Google Maps API Key not found (try adding GOOGLE_MAPS_API_KEY to your settings).') else: - self._key = key + 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' + self.version = getattr(settings, 'GOOGLE_MAPS_API_VERSION', '2.x') else: - self._version = version + 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 + self.api_url = mark_safe(getattr(settings, 'GOOGLE_MAPS_URL', GOOGLE_MAPS_URL) % self.version) else: - self._url = api_url + self.api_url = api_url - # Setting the DOM id of the map, the center lat/lon, the load function, - # and the zoom. + # Setting the DOM id of the map, the load function, the JavaScript + # template, and the KML URLs array. 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 + self.kml_urls = kml_urls + + # Does the user want any GPolygon or GPolyline overlays? + self.polygons, self.polylines = [], [] + if polygons: + for poly in polygons: + if isinstance(poly, GPolygon): + self.polygons.append(poly) + else: + self.polygons.append(GPolygon(poly)) + if polylines: + for pline in polylines: + if isinstance(pline, GPolyline): + self.polylines.append(pline) + else: + self.polylines.append(GPolyline(pline)) + + # Automatically determining the zoom level if there are + # GPolygon and/or GPolyline overlays. + if (self.polygons or self.polylines) and zoom is None: + envelopes = [p.envelope for p in self.polygons] + envelopes.extend([p.envelope for p in self.polylines]) + # Creating a MultiPolygon of all the envelopes, this will + # be used in determining the zoom level. + zoom = geos.MultiPolygon(envelopes) + zoom.srid = 4326 + + # If a GEOSGeometry object is passed in for the `zoom` keyword + # argument, then try to automatically determine an appropriate + # zoom level. + if isinstance(zoom, geos.GEOSGeometry): + self.zoom = GZOOM.get_zoom(zoom) + else: + self.zoom = zoom + + # The map center coordinate -- the `center_lon` and `center_lat` keyword + # are deprecated. + if not center: + center = (center_lon, center_lat) + self.center = center # 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, + params = {'center' : self.center, + 'dom_id' : self.dom_id, + 'kml_urls' : self.kml_urls, + 'load_func' : self.load_func, + 'zoom' : self.zoom, + 'polygons' : self.polygons, + 'polylines' : self.polylines, } params.update(extra_context) - self.js = render_to_string(template, params) + self.js = render_to_string(self.template, params) @property def body(self): "Returns HTML body tag for loading and unloading Google Maps javascript." - return '' % (self.onload, self.onunload) + return mark_safe('' % (self.onload, self.onunload)) @property def onload(self): "Returns the `onload` HTML attribute." - return 'onload="%s()"' % self.load_func + return mark_safe('onload="%s()"' % self.load_func) @property def api_script(self): "Returns the ' % (self._url, self._key) + return mark_safe('' % (self.api_url, self.key)) @property def scripts(self): "Returns all tags required for Google Maps JavaScript." - return '%s\n ' % (self.api_script, self.js) + return mark_safe('%s\n ' % (self.api_script, self.js)) @property def style(self): "Returns additional CSS styling needed for Google Maps on IE." - return '' % self.vml_css + return mark_safe('' % self.vml_css) @property def xhtml(self): "Returns XHTML information needed for IE VML overlays." - return '' % self.xmlns + return mark_safe('' % self.xmlns) diff --git a/django/contrib/gis/maps/google/overlays.py b/django/contrib/gis/maps/google/overlays.py new file mode 100644 index 0000000000..921fe34f3a --- /dev/null +++ b/django/contrib/gis/maps/google/overlays.py @@ -0,0 +1,101 @@ +from django.contrib.gis.geos import LineString, LinearRing, Polygon +from django.utils.safestring import mark_safe + +class GOverlayBase(object): + def latlng_from_coords(self, coords): + return '[%s]' % ','.join(['new GLatLng(%s,%s)' % (y, x) for x, y in coords]) + + def __unicode__(self): + "The string representation is the JavaScript API call." + return mark_safe('%s(%s)' % (self.__class__.__name__, self.js_params)) + +class GPolygon(GOverlayBase): + """ + A Python wrapper for the Google GPolygon object. For more information + please see the Google Maps API Reference: + http://code.google.com/apis/maps/documentation/reference.html#GPolygon + """ + def __init__(self, poly, + stroke_color='#0000ff', stroke_weight=2, stroke_opacity=1, + fill_color='#0000ff', fill_opacity=0.4): + """ + The GPolygon object initializes on a GEOS Polygon. Please note that + this will not depict Polygons with internal rings. + + Keyword Options: + + stroke_color: + The color of the polygon outline. Defaults to '#0000ff' (blue). + + stroke_weight: + The width of the polygon outline, in pixels. Defaults to 2. + + stroke_opacity: + The opacity of the polygon outline, between 0 and 1. Defaults to 1. + + fill_color: + The color of the polygon fill. Defaults to '#0000ff' (blue). + + fill_opacity: + The opacity of the polygon fill. Defaults to 0.4. + """ + + # TODO: Take other types of geometries. + if not isinstance(poly, Polygon): + raise TypeError('GPolygon may only initialize on GEOS Polygons.') + + # Getting the envelope of the input polygon (used for automatically + # determining the zoom level). + self.envelope = poly.envelope + + # Translating the coordinates into a JavaScript array of + # Google `GLatLng` objects. + self.points = self.latlng_from_coords(poly.shell.coords) + + # Stroke settings. + self.stroke_color, self.stroke_opacity, self.stroke_weight = stroke_color, stroke_opacity, stroke_weight + + # Fill settings. + self.fill_color, self.fill_opacity = fill_color, fill_opacity + + @property + def js_params(self): + return '%s, "%s", %s, %s, "%s", %s' % (self.points, self.stroke_color, self.stroke_weight, self.stroke_opacity, + self.fill_color, self.fill_opacity) + +class GPolyline(GOverlayBase): + """ + A Python wrapper for the Google GPolyline object. For more information + please see the Google Maps API Reference: + http://code.google.com/apis/maps/documentation/reference.html#GPolyline + """ + def __init__(self, geom, color='#0000ff', weight=2, opacity=1): + """ + The GPolyline object may initialize on GEOS LineStirng, LinearRing, + and Polygon objects (internal rings not supported). + + Keyword Options: + + color: + The color to use for the polyline. Defaults to '#0000ff' (blue). + + weight: + The width of the polyline, in pixels. Defaults to 2. + + opacity: + The opacity of the polyline, between 0 and 1. Defaults to 1. + """ + if isinstance(geom, (LineString, LinearRing)): + self.latlngs = self.latlng_from_coords(geom.coords) + elif isinstance(geom, Polygon): + self.latlngs = self.latlng_from_coords(geom.shell.coords) + else: + raise TypeError('GPolyline may only initialize on GEOS LineString, LinearRing, and/or Polygon geometries.') + + # Getting the envelope for automatic zoom determination. + self.envelope = geom.envelope + self.color, self.weight, self.opacity = color, weight, opacity + + @property + def js_params(self): + return '%s, "%s", %s, %s' % (self.latlngs, self.color, self.weight, self.opacity) diff --git a/django/contrib/gis/maps/google/zoom.py b/django/contrib/gis/maps/google/zoom.py index 16b18ac555..b94fa31157 100644 --- a/django/contrib/gis/maps/google/zoom.py +++ b/django/contrib/gis/maps/google/zoom.py @@ -99,7 +99,7 @@ class GoogleZoom(object): 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.' + raise TypeError('Pixel should be a sequence of two elements.') # Getting the number of pixels for the given zoom level. npix = self._npix[zoom] @@ -138,8 +138,8 @@ class GoogleZoom(object): "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.' + if not isinstance(geom, GEOSGeometry) or 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. @@ -156,9 +156,9 @@ class GoogleZoom(object): # 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.' + 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/templates/gis/google/js/google-map.js b/django/contrib/gis/templates/gis/google/js/google-map.js index cb78acf5a8..b8afbf11e5 100644 --- a/django/contrib/gis/templates/gis/google/js/google-map.js +++ b/django/contrib/gis/templates/gis/google/js/google-map.js @@ -1,16 +1,20 @@ -{% block vars %}var map;{% for kml_url in kml_urls %}var kml{{ forloop.counter }};{% endfor %}{% endblock %} +{% autoescape off %}{% block vars %}var map;{% 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 }}"); + {% block controls %}map.addControl(new GSmallMapControl()); + map.addControl(new GMapTypeControl());{% endblock %} + map.setCenter(new GLatLng({{ center.1 }}, {{ center.0 }}), {{ zoom }}); + {% for kml_url in kml_urls %}var kml{{ forloop.counter }} = new GGeoXml("{{ kml_url }}"); map.addOverlay(kml{{ forloop.counter }});{% endfor %} + {% for polygon in polygons %}var poly{{ forloop.counter }} = new {{ polygon }}; + map.addOverlay(poly{{ forloop.counter }});{% endfor %} + {% for polyline in polylines %}var polyline{{ forloop.counter }} = new {{ polyline }}; + map.addOverlay(polyline{{ 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 +{% endblock %}{% endautoescape %}