Index: contrib/gis/admin/options.py
===================================================================
--- contrib/gis/admin/options.py	(revision 11585)
+++ contrib/gis/admin/options.py	(working copy)
@@ -1,130 +1,89 @@
-from django.conf import settings
+import copy
+
+from django.utils.encoding import force_unicode
 from django.contrib.admin import ModelAdmin
-from django.contrib.gis.admin.widgets import OpenLayersWidget
-from django.contrib.gis.gdal import OGRGeomType
 from django.contrib.gis.db import models
+from django.contrib.gis.widgets import EditableMap, InfoMap
 
 class GeoModelAdmin(ModelAdmin):
-    """
-    The administration options class for Geographic models. Map settings
-    may be overloaded from their defaults to create custom maps.
-    """
-    # The default map settings that may be overloaded -- still subject
-    # to API changes.
-    default_lon = 0
-    default_lat = 0
-    default_zoom = 4
-    display_wkt = False
-    display_srid = False
-    extra_js = []
-    num_zoom = 18
-    max_zoom = False
-    min_zoom = False
-    units = False
-    max_resolution = False
-    max_extent = False
-    modifiable = True
-    mouse_position = True
-    scale_text = True
-    layerswitcher = True
-    scrollable = True
-    map_width = 600
-    map_height = 400
-    map_srid = 4326
-    map_template = 'gis/admin/openlayers.html'
-    openlayers_url = 'http://openlayers.org/api/2.8/OpenLayers.js'
-    point_zoom = num_zoom - 6
-    wms_url = 'http://labs.metacarta.com/wms/vmap0'
-    wms_layer = 'basic'
-    wms_name = 'OpenLayers WMS'
-    debug = False
-    widget = OpenLayersWidget
+    options = {}
+    list_map = None
+    list_map_options = {}
+    change_list_template = "gis/admin/olwidget_change_list.html"
 
-    def _media(self):
-        "Injects OpenLayers JavaScript into the admin."
-        media = super(GeoModelAdmin, self)._media()
-        media.add_js([self.openlayers_url])
-        media.add_js(self.extra_js)
-        return media
-    media = property(_media)
-
     def formfield_for_dbfield(self, db_field, **kwargs):
         """
-        Overloaded from ModelAdmin so that an OpenLayersWidget is used
-        for viewing/editing GeometryFields.
+        Overloaded from ModelAdmin to use our map widget.
         """
         if isinstance(db_field, models.GeometryField):
             request = kwargs.pop('request', None)
-            # Setting the widget with the newly defined widget.
             kwargs['widget'] = self.get_map_widget(db_field)
             return db_field.formfield(**kwargs)
         else:
-            return super(GeoModelAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+            return super(GeoModelAdmin, self).formfield_for_dbfield(
+                    db_field, **kwargs)
 
     def get_map_widget(self, db_field):
         """
-        Returns a subclass of the OpenLayersWidget (or whatever was specified
-        in the `widget` attribute) using the settings from the attributes set 
-        in this class.
+        Returns an EditableMap subclass with options appropriate for the given
+        field.
         """
-        is_collection = db_field.geom_type in ('MULTIPOINT', 'MULTILINESTRING', 'MULTIPOLYGON', 'GEOMETRYCOLLECTION')
-        if is_collection:
-            if db_field.geom_type == 'GEOMETRYCOLLECTION': collection_type = 'Any'
-            else: collection_type = OGRGeomType(db_field.geom_type.replace('MULTI', ''))
+        is_collection = db_field.geom_type in ('MULTIPOINT', 'MULTILINESTRING', 
+                'MULTIPOLYGON', 'GEOMETRYCOLLECTION')
+        if db_field.geom_type == 'GEOMETRYCOLLECTION':
+            geometry = ['polygon', 'point', 'linestring']
         else:
-            collection_type = 'None'
+            if db_field.geom_type in ('MULTIPOINT', 'POINT'):
+                geometry = 'point'
+            elif db_field.geom_type in ('POLYGON', 'MULTIPOLYGON'):
+                geometry = 'polygon'
+            elif db_field.geom_type in ('LINESTRING', 'MULTILINESTRING'):
+                geometry = 'linestring'
+            else:
+                # fallback: allow all types.
+                geometry = ['polygon', 'point', 'linestring']
 
-        class OLMap(self.widget):
-            template = self.map_template
-            geom_type = db_field.geom_type
-            params = {'default_lon' : self.default_lon,
-                      'default_lat' : self.default_lat,
-                      'default_zoom' : self.default_zoom,
-                      'display_wkt' : self.debug or self.display_wkt,
-                      'geom_type' : OGRGeomType(db_field.geom_type),
-                      'field_name' : db_field.name,
-                      'is_collection' : is_collection,
-                      'scrollable' : self.scrollable,
-                      'layerswitcher' : self.layerswitcher,
-                      'collection_type' : collection_type,
-                      'is_linestring' : db_field.geom_type in ('LINESTRING', 'MULTILINESTRING'),
-                      'is_polygon' : db_field.geom_type in ('POLYGON', 'MULTIPOLYGON'),
-                      'is_point' : db_field.geom_type in ('POINT', 'MULTIPOINT'),
-                      'num_zoom' : self.num_zoom,
-                      'max_zoom' : self.max_zoom,
-                      'min_zoom' : self.min_zoom,
-                      'units' : self.units, #likely shoud get from object
-                      'max_resolution' : self.max_resolution,
-                      'max_extent' : self.max_extent,
-                      'modifiable' : self.modifiable,
-                      'mouse_position' : self.mouse_position,
-                      'scale_text' : self.scale_text,
-                      'map_width' : self.map_width,
-                      'map_height' : self.map_height,
-                      'point_zoom' : self.point_zoom,
-                      'srid' : self.map_srid,
-                      'display_srid' : self.display_srid,
-                      'wms_url' : self.wms_url,
-                      'wms_layer' : self.wms_layer,
-                      'wms_name' : self.wms_name,
-                      'debug' : self.debug,
-                      }
-        return OLMap
+        options = copy.deepcopy(self.options) 
+        options.update({
+            'geometry': geometry, 
+            'isCollection': is_collection,
+            'name': db_field.name,
+        })
+        class Widget(EditableMap):
+            def __init__(self, *args, **kwargs):
+                kwargs['options'] = options
+                # OL rendering bug with floats requires this.
+                kwargs['template'] = "gis/admin/admin_editable_map.html"
+                super(Widget, self).__init__(*args, **kwargs)
 
-# Using the Beta OSM in the admin requires the following:
-#  (1) The Google Maps Mercator projection needs to be added
-#      to your `spatial_ref_sys` table.  You'll need at least GDAL 1.5:
-#      >>> from django.contrib.gis.gdal import SpatialReference
-#      >>> from django.contrib.gis.utils import add_postgis_srs
-#      >>> add_postgis_srs(SpatialReference(900913)) # Adding the Google Projection 
-from django.contrib.gis import gdal
-if gdal.HAS_GDAL:
-    class OSMGeoAdmin(GeoModelAdmin):
-        map_template = 'gis/admin/osm.html'
-        extra_js = ['http://openstreetmap.org/openlayers/OpenStreetMap.js']
-        num_zoom = 20
-        map_srid = 900913
-        max_extent = '-20037508,-20037508,20037508,20037508'
-        max_resolution = 156543.0339
-        point_zoom = num_zoom - 6
-        units = 'm'
+        return Widget
+
+    def get_changelist_map(self, cl):
+        """ 
+        Display a map in the admin changelist, with info popups
+        """
+        if self.list_map:
+            info = []
+            for obj in cl.get_query_set():
+                info.append((
+                    [getattr(obj, field) for field in self.list_map], 
+                    "<a href='%s'>%s</a>" % (
+                        cl.url_for_result(obj),
+                        force_unicode(obj)
+                    )
+                ))
+
+            return InfoMap(info, options=self.list_map_options)
+        return None
+
+    def get_changelist_context(self, request):
+        """
+        Add changelist map, if any, to the context.
+        """
+        context = super(GeoModelAdmin, self).get_changelist_context(request)
+        map = self.get_changelist_map(context['cl'])
+        if map:
+            context['media'] += map.media
+            context['map'] = map
+        return context
+
Index: contrib/gis/admin/__init__.py
===================================================================
--- contrib/gis/admin/__init__.py	(revision 11585)
+++ contrib/gis/admin/__init__.py	(working copy)
@@ -1,12 +1,5 @@
 # Getting the normal admin routines, classes, and `site` instance.
 from django.contrib.admin import autodiscover, site, AdminSite, ModelAdmin, StackedInline, TabularInline, HORIZONTAL, VERTICAL
 
-# Geographic admin options classes and widgets.
+# Geographic admin options classes
 from django.contrib.gis.admin.options import GeoModelAdmin
-from django.contrib.gis.admin.widgets import OpenLayersWidget
-
-try:
-    from django.contrib.gis.admin.options import OSMGeoAdmin
-    HAS_OSM = True
-except ImportError:
-    HAS_OSM = False
Index: contrib/gis/admin/widgets.py
===================================================================
--- contrib/gis/admin/widgets.py	(revision 11585)
+++ contrib/gis/admin/widgets.py	(working copy)
@@ -1,107 +0,0 @@
-from django.conf import settings
-from django.contrib.gis.gdal import OGRException
-from django.contrib.gis.geos import GEOSGeometry, GEOSException
-from django.forms.widgets import Textarea
-from django.template import loader, Context
-from django.utils import translation
-
-# Creating a template context that contains Django settings
-# values needed by admin map templates.
-geo_context = Context({'ADMIN_MEDIA_PREFIX' : settings.ADMIN_MEDIA_PREFIX,
-                       'LANGUAGE_BIDI' : translation.get_language_bidi(),
-                       })
-
-class OpenLayersWidget(Textarea):
-    """
-    Renders an OpenLayers map using the WKT of the geometry.
-    """
-    def render(self, name, value, attrs=None):
-        # Update the template parameters with any attributes passed in.
-        if attrs: self.params.update(attrs)
-
-        # Defaulting the WKT value to a blank string -- this
-        # will be tested in the JavaScript and the appropriate
-        # interface will be constructed.
-        self.params['wkt'] = ''
-
-        # If a string reaches here (via a validation error on another
-        # field) then just reconstruct the Geometry.
-        if isinstance(value, basestring):
-            try:
-                value = GEOSGeometry(value)
-            except (GEOSException, ValueError):
-                value = None
-
-        if value and value.geom_type.upper() != self.geom_type:
-            value = None
-
-        # Constructing the dictionary of the map options.
-        self.params['map_options'] = self.map_options()
-
-        # Constructing the JavaScript module name using the name of
-        # the GeometryField (passed in via the `attrs` keyword).
-        # Use the 'name' attr for the field name (rather than 'field')
-        self.params['name'] = name
-        # note: we must switch out dashes for underscores since js
-        # functions are created using the module variable
-        js_safe_name = self.params['name'].replace('-','_')
-        self.params['module'] = 'geodjango_%s' % js_safe_name
-
-        if value:
-            # Transforming the geometry to the projection used on the
-            # OpenLayers map.
-            srid = self.params['srid']
-            if value.srid != srid:
-                try:
-                    ogr = value.ogr
-                    ogr.transform(srid)
-                    wkt = ogr.wkt
-                except OGRException:
-                    wkt = ''
-            else:
-                wkt = value.wkt
-
-            # Setting the parameter WKT with that of the transformed
-            # geometry.
-            self.params['wkt'] = wkt
-
-        return loader.render_to_string(self.template, self.params,
-                                       context_instance=geo_context)
-
-    def map_options(self):
-        "Builds the map options hash for the OpenLayers template."
-
-        # JavaScript construction utilities for the Bounds and Projection.
-        def ol_bounds(extent):
-            return 'new OpenLayers.Bounds(%s)' % str(extent)
-        def ol_projection(srid):
-            return 'new OpenLayers.Projection("EPSG:%s")' % srid
-
-        # An array of the parameter name, the name of their OpenLayers
-        # counterpart, and the type of variable they are.
-        map_types = [('srid', 'projection', 'srid'),
-                     ('display_srid', 'displayProjection', 'srid'),
-                     ('units', 'units', str),
-                     ('max_resolution', 'maxResolution', float),
-                     ('max_extent', 'maxExtent', 'bounds'),
-                     ('num_zoom', 'numZoomLevels', int),
-                     ('max_zoom', 'maxZoomLevels', int),
-                     ('min_zoom', 'minZoomLevel', int),
-                     ]
-
-        # Building the map options hash.
-        map_options = {}
-        for param_name, js_name, option_type in map_types:
-            if self.params.get(param_name, False):
-                if option_type == 'srid':
-                    value = ol_projection(self.params[param_name])
-                elif option_type == 'bounds':
-                    value = ol_bounds(self.params[param_name])
-                elif option_type in (float, int):
-                    value = self.params[param_name]
-                elif option_type in (str,):
-                    value = '"%s"' % self.params[param_name]
-                else:
-                    raise TypeError
-                map_options[js_name] = value
-        return map_options
Index: contrib/gis/widgets.py
===================================================================
--- contrib/gis/widgets.py	(revision 0)
+++ contrib/gis/widgets.py	(revision 0)
@@ -0,0 +1,279 @@
+from os.path import join
+
+from django.contrib.gis.gdal import OGRException
+from django.contrib.gis.geos import GEOSGeometry, GEOSException
+from django.forms.widgets import Textarea
+from django.template.loader import render_to_string
+from django.utils import simplejson
+from django.conf import settings
+from django import forms
+
+try:
+    GOOGLE_API_KEY = settings.GOOGLE_API_KEY
+except AttributeError:
+    GOOGLE_API_KEY = ""
+
+try:
+    YAHOO_APP_ID = settings.YAHOO_APP_ID
+except AttributeError:
+    YAHOO_APP_ID = ""
+
+
+GOOGLE_API = "http://maps.google.com/maps?file=api&v=2&key=%s" % GOOGLE_API_KEY
+YAHOO_API = "http://api.maps.yahoo.com/ajaxymap?v=3.0&appid=%s" % YAHOO_APP_ID
+MS_VE_API = "http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6.1"
+OSM_API = "http://openstreetmap.org/openlayers/OpenStreetMap.js"
+OL_API = "http://openlayers.org/api/2.8/OpenLayers.js"
+OLWIDGET_JS = join(settings.ADMIN_MEDIA_PREFIX, "js/olwidget.js")
+OLWIDGET_CSS = join(settings.ADMIN_MEDIA_PREFIX, "css/olwidget.css")
+
+DEFAULT_PROJ = "4326"
+
+PEP8_OPTIONS_TRANSLATION = {
+    # map constructor options
+    'map_options': 'mapOptions',
+    'max_resolution': 'maxResolution',
+    'min_resolution': 'minResolution',
+    'max_extent': 'maxExtent',
+    'min_extent': 'minExtent',
+    'min_scale': 'minScale',
+    'max_scale': 'maxScale',
+    'display_projection': 'displayProjection',
+    'num_zoom_levels': 'numZoomLevels',
+    # olwidget options
+    'map_div_class': 'mapDivClass',
+    'map_div_style': 'mapDivStyle',
+    'default_lon': 'defaultLon',
+    'default_lat': 'defaultLat',
+    'default_zoom': 'defaultZoom',
+    'hide_textarea': 'hideTextarea',
+    'is_collection': 'isCollection',
+    'popups_outside': 'popupsOutside',
+    'popup_direction': 'popupDirection',
+    'popup_pagination_separator': 'popupPaginationSeparator',
+    'cluster_display': 'clusterDisplay',
+    'cluster_style': 'clusterStyle',
+    'font_size': 'fontSize',
+    'font_family': 'fontFamily',
+    'font_color': 'fontColor',
+    # overlay style options
+    'overlay_style': 'overlayStyle',
+    'fill_color': 'fillColor',
+    'fill_opacity': 'fillOpacity',
+    'stroke_color': 'strokeColor',
+    'stroke_width': 'strokeWidth',
+    'stroke_linecap': 'strokeLinecap',
+    'stroke_dash_style': 'strokeDashstyle',
+    'point_radius': 'pointRadius',
+    'external_graphic': 'externalGraphic',
+    'graphic_height': 'graphicHeight',
+    'graphic_x_offset': 'graphicXOffset',
+    'graphic_y_offset': 'graphicYOffset',
+    'graphic_opacity': 'graphicOpacity',
+    'graphic_name': 'graphicName',
+}
+
+def translate_options(options):
+    translated = {}
+    for key, value in options.iteritems():
+        new_key = PEP8_OPTIONS_TRANSLATION.get(key, key)
+        # recurse
+        if isinstance(value, dict):
+            translated[new_key] = translate_options(value)
+        else:
+            translated[new_key] = value
+    return translated
+
+class MapMixin(object):
+    def set_options(self, options, template):
+        self.options = options or {}
+        # Though this is the olwidget.js default, it must be explicitly set so
+        # form.media knows to include osm.
+        self.options['layers'] = self.options.get('layers', ['osm.mapnik'])
+        self.template = template or self.default_template
+
+    def _media(self):
+        js = set()
+        # collect scripts necessary for various layers
+        for layer in self.options['layers']:
+            if layer.startswith("osm."):
+                js.add(OSM_API)
+            elif layer.startswith("google."):
+                js.add(GOOGLE_API)
+            elif layer.startswith("yahoo."):
+                js.add(YAHOO_API)
+            elif layer.startswith("ve."):
+                js.add(MS_VE_API)
+        js = [OL_API, OLWIDGET_JS] + list(js)
+        return forms.Media(css={'all': (OLWIDGET_CSS,)}, js=js)
+    media = property(_media)
+
+class EditableMap(forms.Textarea, MapMixin):
+    """
+    An OpenLayers mapping widget for geographic data.
+
+    Example::
+
+        from django import forms
+        from olwidget.widgets import OLWidget
+
+        class MyForm(forms.Form):
+            location = forms.CharField(widget=EditableMap(
+                options={'geometry': 'point'}))
+    """
+    default_template = 'gis/editable_map.html'
+
+    def __init__(self, options=None, template=None):
+        self.set_options(options, template)
+        super(EditableMap, self).__init__()
+
+    def render(self, name, value, attrs=None):
+        if not attrs:
+            attrs = {}
+        # without an id, javascript fails
+        if attrs.has_key('id'):
+            element_id = attrs['id']
+        else:
+            element_id = "id_%s" % id(self)
+
+        # Allow passing of wkt for MapDisplay subclass
+        if attrs.has_key('wkt'):
+            wkt = attrs['wkt']
+        else:
+            # Use the default SRID's
+            wkt = add_srid(get_wkt(value))
+
+        if name and not self.options.has_key('name'):
+            self.options['name'] = name
+
+        context = {
+            'id': element_id,
+            'name': name,
+            'wkt': wkt,
+            'map_opts': simplejson.dumps(
+                translate_options(self.options)
+            ),
+        }
+        return render_to_string(self.template, context)
+
+class MapDisplay(EditableMap):
+    """
+    Object for display of geometries on an OpenLayers map.  Arguments (all are
+    optional):
+
+    * ``fields`` - a list of geometric fields or WKT strings to display on the
+      map.  If none are given, the map will have no overlay.
+    * ``name`` - a name to use for display of the field data layer.
+    * ``options`` - a dict of options for map display.  A complete list of
+      options is in the documentation for olwidget.js.
+    """
+
+    def __init__(self, fields=None, options=None, template=None):
+        self.fields = fields
+
+        options = options or {}
+        if not options.has_key('editable'):
+            options['editable'] = False
+
+        if (self.fields and len(self.fields) > 1) or \
+                (fields[0].geom_type.upper() == 'GEOMETRYCOLLECTION'):
+            options['isCollection'] = True
+
+        super(MapDisplay, self).__init__(options, template)
+
+    def __unicode__(self):
+        wkt = add_srid(collection_wkt(self.fields))
+        name = self.options.get('name', 'data')
+        return self.render(name, None, attrs={'wkt': wkt})
+
+class InfoMap(forms.Widget, MapMixin):
+    """
+    Widget for displaying maps with pop-up info boxes over geometries.
+    Arguments:
+
+    * ``info``: an array of [geometry, HTML] pairs that specify geometries, and
+      the popup contents associated with them. Geometries can be expressed as
+      geometry fields, or as WKT strings.  Example::
+
+          [
+            [geomodel1.geofield, "<p>Model One</p>"],
+            [geomodel2.geofield, "<p>Model Two</p>"],
+            ...
+          ]
+
+    * ``options``: an optional dict of options for map display.
+      
+    """
+    default_template = 'gis/info_map.html'
+
+    def __init__(self, info=None, options=None, template=None):
+        self.info = info
+        self.set_options(options, template)
+        super(InfoMap, self).__init__()
+
+    def render(self, name, value, attrs=None):
+        if self.info is None:
+            info_json = '[]'
+        else:
+            # convert fields to wkt
+            for geom, html in self.info:
+                wkt_array = [[add_srid(collection_wkt(geom)), html] for geom, html in self.info]
+            info_json = simplejson.dumps(wkt_array)
+
+        # arbitrary unique id
+        div_id = "id_%s" % id(self)
+
+        context = {
+            'id': div_id,
+            'info_array': info_json,
+            'map_opts': simplejson.dumps(
+                translate_options(self.options)
+            ),
+        }
+        return render_to_string(self.template, context)
+
+    def __unicode__(self):
+        return self.render(None, None)
+
+def get_wkt(value, srid=DEFAULT_PROJ):
+    """
+    `value` is either a WKT string or a geometry field.  Returns WKT in the
+    projection for the given SRID.
+    """
+    if isinstance(value, basestring):
+        try:
+            value = GEOSGeometry(value)
+        except (GEOSException, ValueError):
+            value = None
+
+    wkt = ''
+    if value:
+        try:
+            ogr = value.ogr
+            ogr.transform(srid)
+            wkt = ogr.wkt
+        except OGRException:
+            pass
+    return wkt
+
+def collection_wkt(fields):
+    """ Returns WKT for the given list of geometry fields. """
+
+    if not fields:
+        return ""
+
+    if len(fields) == 1:
+        return get_wkt(fields[0])
+
+    return "GEOMETRYCOLLECTION(%s)" % \
+            ",".join(get_wkt(field) for field in fields)
+
+def add_srid(wkt, srid=DEFAULT_PROJ):
+    """
+    Returns EWKT (WKT with a specified SRID) for the given wkt and SRID
+    (default 4326). 
+    """
+    if wkt:
+        return "SRID=%s;%s" % (srid, wkt)
+    return ""
+
Index: contrib/gis/templates/gis/admin/osm.html
===================================================================
--- contrib/gis/templates/gis/admin/osm.html	(revision 11585)
+++ contrib/gis/templates/gis/admin/osm.html	(working copy)
@@ -1,2 +0,0 @@
-{% extends "gis/admin/openlayers.html" %}
-{% block openlayers %}{% include "gis/admin/osm.js" %}{% endblock %}
\ No newline at end of file
Index: contrib/gis/templates/gis/admin/admin_editable_map.html
===================================================================
--- contrib/gis/templates/gis/admin/admin_editable_map.html	(revision 0)
+++ contrib/gis/templates/gis/admin/admin_editable_map.html	(revision 0)
@@ -0,0 +1,4 @@
+<span style='float: left;'>
+    {% include "gis/editable_map.html" %}
+</span>
+<div style='clear: both;'></div>
Index: contrib/gis/templates/gis/admin/olwidget_change_list.html
===================================================================
--- contrib/gis/templates/gis/admin/olwidget_change_list.html	(revision 0)
+++ contrib/gis/templates/gis/admin/olwidget_change_list.html	(revision 0)
@@ -0,0 +1,9 @@
+{% extends "admin/change_list.html" %}
+
+{% block content %}
+    {% if not is_popup %}
+        {{ map }}
+    {% endif %}
+
+    {{ block.super }}
+{% endblock %}
Index: contrib/gis/templates/gis/admin/openlayers.html
===================================================================
--- contrib/gis/templates/gis/admin/openlayers.html	(revision 11585)
+++ contrib/gis/templates/gis/admin/openlayers.html	(working copy)
@@ -1,37 +0,0 @@
-{% block extrastyle %}
-<style type="text/css">
-  #{{ id }}_map { width: {{ map_width }}px; height: {{ map_height }}px; }
-  #{{ id }}_map .aligned label { float:inherit; }
-  #{{ id }}_admin_map { position: relative; vertical-align: top; float: {{ LANGUAGE_BIDI|yesno:"right,left" }}; }
-  {% if not display_wkt %}#{{ id }} { display: none; }{% endif %}
-  .olControlEditingToolbar .olControlModifyFeatureItemActive { 
-     background-image: url("{{ ADMIN_MEDIA_PREFIX }}img/gis/move_vertex_on.png");
-     background-repeat: no-repeat;
-  }
-  .olControlEditingToolbar .olControlModifyFeatureItemInactive { 
-     background-image: url("{{ ADMIN_MEDIA_PREFIX }}img/gis/move_vertex_off.png");
-     background-repeat: no-repeat;
-  }
-</style>
-<!--[if IE]>
-<style type="text/css">
-  /* This fixes the mouse offset issues in IE. */
-  #{{ id }}_admin_map { position: static; vertical-align: top; }
-  /* `font-size: 0` fixes the 1px border between tiles, but borks LayerSwitcher.
-      Thus, this is disabled until a better fix is found.
-  #{{ id }}_map { width: {{ map_width }}px; height: {{ map_height }}px; font-size: 0; } */
-</style>
-<![endif]-->
-{% endblock %}
-<span id="{{ id }}_admin_map">
-<script type="text/javascript">
-//<![CDATA[
-{% block openlayers %}{% include "gis/admin/openlayers.js" %}{% endblock %}
-//]]>
-</script>
-<div id="{{ id }}_map"{% if LANGUAGE_BIDI %} dir="ltr"{% endif %}></div>
-<a href="javascript:{{ module }}.clearFeatures()">Delete all Features</a>
-{% if display_wkt %}<p> WKT debugging window:</p>{% endif %}
-<textarea id="{{ id }}" class="vWKTField required" cols="150" rows="10" name="{{ name }}">{{ wkt }}</textarea>
-<script type="text/javascript">{% block init_function %}{{ module }}.init();{% endblock %}</script>
-</span>
Index: contrib/gis/templates/gis/admin/osm.js
===================================================================
--- contrib/gis/templates/gis/admin/osm.js	(revision 11585)
+++ contrib/gis/templates/gis/admin/osm.js	(working copy)
@@ -1,2 +0,0 @@
-{% extends "gis/admin/openlayers.js" %}
-{% block base_layer %}new OpenLayers.Layer.OSM.Mapnik("OpenStreetMap (Mapnik)");{% endblock %}
Index: contrib/gis/templates/gis/admin/openlayers.js
===================================================================
--- contrib/gis/templates/gis/admin/openlayers.js	(revision 11585)
+++ contrib/gis/templates/gis/admin/openlayers.js	(working copy)
@@ -1,160 +0,0 @@
-{# Author: Justin Bronn, Travis Pinney & Dane Springmeyer #}
-{% block vars %}var {{ module }} = {};
-{{ module }}.map = null; {{ module }}.controls = null; {{ module }}.panel = null; {{ module }}.re = new RegExp("^SRID=\d+;(.+)", "i"); {{ module }}.layers = {}; 
-{{ module }}.wkt_f = new OpenLayers.Format.WKT();
-{{ module }}.is_collection = {{ is_collection|yesno:"true,false" }};
-{{ module }}.collection_type = '{{ collection_type }}';
-{{ module }}.is_linestring = {{ is_linestring|yesno:"true,false" }};
-{{ module }}.is_polygon = {{ is_polygon|yesno:"true,false" }};
-{{ module }}.is_point = {{ is_point|yesno:"true,false" }};
-{% endblock %}
-{{ module }}.get_ewkt = function(feat){return 'SRID={{ srid }};' + {{ module }}.wkt_f.write(feat);}
-{{ module }}.read_wkt = function(wkt){
-  // OpenLayers cannot handle EWKT -- we make sure to strip it out.
-  // EWKT is only exposed to OL if there's a validation error in the admin.
-  var match = {{ module }}.re.exec(wkt);
-  if (match){wkt = match[1];}
-  return {{ module }}.wkt_f.read(wkt);
-}
-{{ module }}.write_wkt = function(feat){
-  if ({{ module }}.is_collection){ {{ module }}.num_geom = feat.geometry.components.length;}
-  else { {{ module }}.num_geom = 1;}
-  document.getElementById('{{ id }}').value = {{ module }}.get_ewkt(feat);
-}
-{{ module }}.add_wkt = function(event){
-  // This function will sync the contents of the `vector` layer with the
-  // WKT in the text field.
-  if ({{ module }}.is_collection){
-    var feat = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.{{ geom_type }}());
-    for (var i = 0; i < {{ module }}.layers.vector.features.length; i++){
-      feat.geometry.addComponents([{{ module }}.layers.vector.features[i].geometry]);
-    }
-    {{ module }}.write_wkt(feat);
-  } else {
-    // Make sure to remove any previously added features.
-    if ({{ module }}.layers.vector.features.length > 1){
-      old_feats = [{{ module }}.layers.vector.features[0]];
-      {{ module }}.layers.vector.removeFeatures(old_feats);
-      {{ module }}.layers.vector.destroyFeatures(old_feats);
-    }
-    {{ module }}.write_wkt(event.feature);
-  }
-}
-{{ module }}.modify_wkt = function(event){
-  if ({{ module }}.is_collection){
-    if ({{ module }}.is_point){
-      {{ module }}.add_wkt(event); 
-      return;
-    } else {
-      // When modifying the selected components are added to the 
-      // vector layer so we only increment to the `num_geom` value.
-      var feat = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.{{ geom_type }}());
-      for (var i = 0; i < {{ module }}.num_geom; i++){
-	feat.geometry.addComponents([{{ module }}.layers.vector.features[i].geometry]);
-      }
-      {{ module }}.write_wkt(feat);
-    }
-  } else {
-    {{ module }}.write_wkt(event.feature);
-  }
-}
-// Function to clear vector features and purge wkt from div
-{{ module }}.deleteFeatures = function(){
-  {{ module }}.layers.vector.removeFeatures({{ module }}.layers.vector.features);
-  {{ module }}.layers.vector.destroyFeatures();
-}
-{{ module }}.clearFeatures = function (){
-  {{ module }}.deleteFeatures();
-  document.getElementById('{{ id }}').value = '';
-  {{ module }}.map.setCenter(new OpenLayers.LonLat({{ default_lon }}, {{ default_lat }}), {{ default_zoom }});
-}
-// Add Select control
-{{ module }}.addSelectControl = function(){   
-  var select = new OpenLayers.Control.SelectFeature({{ module }}.layers.vector, {'toggle' : true, 'clickout' : true});
-  {{ module }}.map.addControl(select);
-  select.activate();
-}
-{{ module }}.enableDrawing = function(){ {{ module }}.map.getControlsByClass('OpenLayers.Control.DrawFeature')[0].activate();}
-{{ module }}.enableEditing = function(){ {{ module }}.map.getControlsByClass('OpenLayers.Control.ModifyFeature')[0].activate();}
-// Create an array of controls based on geometry type
-{{ module }}.getControls = function(lyr){
-  {{ module }}.panel = new OpenLayers.Control.Panel({'displayClass': 'olControlEditingToolbar'});
-  var nav = new OpenLayers.Control.Navigation();
-  var draw_ctl;
-  if ({{ module }}.is_linestring){
-    draw_ctl = new OpenLayers.Control.DrawFeature(lyr, OpenLayers.Handler.Path, {'displayClass': 'olControlDrawFeaturePath'});
-  } else if ({{ module }}.is_polygon){
-    draw_ctl = new OpenLayers.Control.DrawFeature(lyr, OpenLayers.Handler.Polygon, {'displayClass': 'olControlDrawFeaturePolygon'});
-  } else if ({{ module }}.is_point){
-    draw_ctl = new OpenLayers.Control.DrawFeature(lyr, OpenLayers.Handler.Point, {'displayClass': 'olControlDrawFeaturePoint'});
-  }
-  {% if modifiable %}
-  var mod = new OpenLayers.Control.ModifyFeature(lyr, {'displayClass': 'olControlModifyFeature'});
-  {{ module }}.controls = [nav, draw_ctl, mod];
-  {% else %}
-  {{ module }}.controls = [nav, darw_ctl];
-  {% endif %}  
-}
-{{ module }}.init = function(){
-    {% block map_options %}// The options hash, w/ zoom, resolution, and projection settings.
-    var options = { 
-{% autoescape off %}{% for item in map_options.items %}      '{{ item.0 }}' : {{ item.1 }}{% if not forloop.last %},{% endif %}
-{% endfor %}{% endautoescape %}    };{% endblock %}
-    // The admin map for this geometry field.
-    {{ module }}.map = new OpenLayers.Map('{{ id }}_map', options);
-    // Base Layer
-    {{ module }}.layers.base = {% block base_layer %}new OpenLayers.Layer.WMS( "{{ wms_name }}", "{{ wms_url }}", {layers: '{{ wms_layer }}'} );{% endblock %}
-    {{ module }}.map.addLayer({{ module }}.layers.base);
-    {% block extra_layers %}{% endblock %}
-    {% if is_linestring %}OpenLayers.Feature.Vector.style["default"]["strokeWidth"] = 3; // Default too thin for linestrings. {% endif %}
-    {{ module }}.layers.vector = new OpenLayers.Layer.Vector(" {{ field_name }}");
-    {{ module }}.map.addLayer({{ module }}.layers.vector);
-    // Read WKT from the text field.
-    var wkt = document.getElementById('{{ id }}').value;
-    if (wkt){
-      // After reading into geometry, immediately write back to 
-      // WKT <textarea> as EWKT (so that SRID is included).
-      var admin_geom = {{ module }}.read_wkt(wkt);
-      {{ module }}.write_wkt(admin_geom);
-      if ({{ module }}.is_collection){
-	// If geometry collection, add each component individually so they may be
-	// edited individually.
-	for (var i = 0; i < {{ module }}.num_geom; i++){
-	  {{ module }}.layers.vector.addFeatures([new OpenLayers.Feature.Vector(admin_geom.geometry.components[i].clone())]);
-	}
-      } else {
-	{{ module }}.layers.vector.addFeatures([admin_geom]);
-      }
-      // Zooming to the bounds.
-      {{ module }}.map.zoomToExtent(admin_geom.geometry.getBounds());
-      if ({{ module }}.is_point){
-          {{ module }}.map.zoomTo({{ point_zoom }}); 
-      }
-    } else {
-      {{ module }}.map.setCenter(new OpenLayers.LonLat({{ default_lon }}, {{ default_lat }}), {{ default_zoom }});
-    }
-    // This allows editing of the geographic fields -- the modified WKT is
-    // written back to the content field (as EWKT, so that the ORM will know
-    // to transform back to original SRID). 
-    {{ module }}.layers.vector.events.on({"featuremodified" : {{ module }}.modify_wkt});
-    {{ module }}.layers.vector.events.on({"featureadded" : {{ module }}.add_wkt});
-    {% block controls %}
-    // Map controls:
-    // Add geometry specific panel of toolbar controls
-    {{ module }}.getControls({{ module }}.layers.vector);
-    {{ module }}.panel.addControls({{ module }}.controls);
-    {{ module }}.map.addControl({{ module }}.panel);
-    {{ module }}.addSelectControl();
-    // Then add optional visual controls
-    {% if mouse_position %}{{ module }}.map.addControl(new OpenLayers.Control.MousePosition());{% endif %}
-    {% if scale_text %}{{ module }}.map.addControl(new OpenLayers.Control.Scale());{% endif %}
-    {% if layerswitcher %}{{ module }}.map.addControl(new OpenLayers.Control.LayerSwitcher());{% endif %}
-    // Then add optional behavior controls
-    {% if not scrollable %}{{ module }}.map.getControlsByClass('OpenLayers.Control.Navigation')[0].disableZoomWheel();{% endif %}
-    {% endblock %}
-    if (wkt){
-      {{ module }}.enableEditing();
-    } else {
-      {{ module }}.enableDrawing();
-    }
-}
Index: contrib/gis/templates/gis/editable_map.html
===================================================================
--- contrib/gis/templates/gis/editable_map.html	(revision 0)
+++ contrib/gis/templates/gis/editable_map.html	(revision 0)
@@ -0,0 +1,4 @@
+<textarea id="{{ id }}" name="{{ name }}">{{ wkt }}</textarea>
+<script type='text/javascript'>
+    new olwidget.EditableMap("{{ id }}", {{map_opts|safe}});
+</script>
Index: contrib/gis/templates/gis/info_map.html
===================================================================
--- contrib/gis/templates/gis/info_map.html	(revision 0)
+++ contrib/gis/templates/gis/info_map.html	(revision 0)
@@ -0,0 +1,4 @@
+<div id="{{ id }}"></div>
+<script type='text/javascript'>
+    new olwidget.InfoMap("{{ id }}", {{ info_array|safe }}, {{ map_opts|safe }});
+</script>
Index: contrib/admin/media/css/olwidget.css
===================================================================
--- contrib/admin/media/css/olwidget.css	(revision 0)
+++ contrib/admin/media/css/olwidget.css	(revision 0)
@@ -0,0 +1,112 @@
+/*
+ * Extra editing icons
+ */
+.olControlEditingToolbar .olControlModifyFeatureItemInactive {
+    background: url("../img/gis/extra_edit_icons.png") no-repeat scroll -48px 0px;
+}
+.olControlEditingToolbar .olControlModifyFeatureItemActive {
+    background: url("../img/gis/extra_edit_icons.png") no-repeat scroll -24px 0px;
+}
+.olControlEditingToolbar .olControlClearFeaturesItemInactive,
+.olControlEditingToolbar .olControlClearFeaturesItemActive {
+    background: url("../img/gis/extra_edit_icons.png") no-repeat scroll -0px 0px;
+}
+div.olControlEditingToolbar {
+    width: 210px;
+}
+
+/* Override padding to avoid position computation errors */
+.olPopupContent {
+    padding: 0px !important;
+    overflow: visible !important;
+}
+/* Show popups over other controls */
+.olPopup {
+    z-index: 1005 !important;
+}
+
+/* Div containing all popup elements */
+.olwidgetPopupContent {
+    overflow: visible;
+    padding: 2px;
+    border: 2px solid #777;
+    background-color: #777;
+}
+
+.olwidgetPopupCloseBox {
+    cursor: pointer;
+    position: absolute;
+    right: 10px;
+    top: 10px;
+    background: #fff url("../img/gis/popup_icons.png") no-repeat scroll -80px 0px;
+    padding-top: 16px;
+    width: 16px;
+    height: 0px;
+    overflow: hidden;
+}
+.olwidgetPopupCloseBox:hover {
+    background-position: -64px 0px;
+}
+/* Div containing content user HTML */
+.olwidgetPopupPage {
+    overflow: auto;
+    background: #fff;
+    padding: 8px;
+    padding-top: 23px;
+
+    /* IE6 fix */
+    zoom: 1;
+}
+
+.olwidgetPopupPagination {
+    clear: both;
+    float: right;
+    background: #fff;
+    padding-left: 5px;
+    padding-right: 5px;
+}
+.olwidgetPaginationPrevious {
+    float: left;
+    cursor: pointer;
+    /* Replace text with background image trick */
+    background: url("../img/gis/popup_icons.png") no-repeat scroll -48px 0px;
+    padding-top: 16px;
+    width: 16px;
+    height: 0px;
+    overflow: hidden;
+}
+.olwidgetPaginationPrevious:hover {
+    background-position: -32px 0px;
+}
+
+/* Div containing page count, e.g. "1 of 5". */
+.olwidgetPaginationCount {
+    float: left;
+    margin-left: 0.5em;
+    margin-right: 0.5em;
+}
+.olwidgetPaginationNext {
+    float: left;
+    cursor: pointer;
+    /* Replace text with background image trick */
+    background: url("../img/gis/popup_icons.png") no-repeat scroll -16px 0px;
+    padding-top: 16px;
+    width: 16px;
+    height: 0px;
+    overflow: hidden;
+}
+.olwidgetPaginationNext:hover {
+    background-position: 0px 0px;
+}
+.olwidgetClusterList {
+    margin: 0;
+    padding-left: 1em;
+}
+
+/* position blocks */
+.olwidgetPopupStemTL, .olwidgetPopupStemTR {
+    background: url("../img/gis/popup_icons.png") no-repeat scroll -124px 0px;
+}
+.olwidgetPopupStemBL, .olwidgetPopupStemBR {
+    background: url("../img/gis/popup_icons.png") no-repeat scroll -96px 0px;
+}
Index: contrib/admin/media/img/gis/popup_icons.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: contrib/admin/media/img/gis/popup_icons.png
___________________________________________________________________
Added: svn:mime-type
   + application/octet-stream

Index: contrib/admin/media/img/gis/extra_edit_icons.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream

Property changes on: contrib/admin/media/img/gis/extra_edit_icons.png
___________________________________________________________________
Added: svn:mime-type
   + application/octet-stream

Index: contrib/admin/media/img/gis/jquery_ui_license.txt
===================================================================
--- contrib/admin/media/img/gis/jquery_ui_license.txt	(revision 0)
+++ contrib/admin/media/img/gis/jquery_ui_license.txt	(revision 0)
@@ -0,0 +1,29 @@
+The file "popup_icons.png" contain images taken from the jquery-ui library,
+under the following license:
+
+Copyright (c) 2009 Paul Bakaus, http://jqueryui.com/
+
+This software consists of voluntary contributions made by many
+individuals (AUTHORS.txt, http://jqueryui.com/about) For exact
+contribution history, see the revision history and logs, available
+at http://jquery-ui.googlecode.com/svn/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
Index: contrib/admin/media/js/olwidget.js
===================================================================
--- contrib/admin/media/js/olwidget.js	(revision 0)
+++ contrib/admin/media/js/olwidget.js	(revision 0)
@@ -0,0 +1,957 @@
+/*
+ *  Simplified open layers mapping widgets.
+ * 
+ *  olwidget.EditableMap(textareaID, options) -- replace a textarea containing
+ *  (E)WKT data with an editable map.
+ *
+ *  olwidget.InfoMap(div_id, data, options) -- replace a div with a map which
+ *  displays the data (an array of [WKT, html] pairs) in clickable popups.
+ *
+ */
+(function() {
+
+/*
+ *  Base namespace and utility functions
+ */
+var olwidget = {
+    /*
+     * WKT transformations
+     */
+    wktFormat: new OpenLayers.Format.WKT(),
+    featureToEWKT: function(feature, proj) {
+        // convert "EPSG:" in projCode to 'SRID='
+        var srid = 'SRID=' + proj.projCode.substring(5) + ';';
+        return srid + this.wktFormat.write(feature);
+    },
+    stripSRIDre: new RegExp("^SRID=\\d+;(.+)", "i"),
+    ewktToFeature: function(wkt) {
+        var match = this.stripSRIDre.exec(wkt);
+        if (match) {
+            wkt = match[1];
+        }
+        return this.wktFormat.read(wkt);
+    },
+    multiGeometryClasses: {
+        'linestring': OpenLayers.Geometry.MultiLineString,
+        'point': OpenLayers.Geometry.MultiPoint,
+        'polygon': OpenLayers.Geometry.MultiPolygon,
+        'collection': OpenLayers.Geometry.Collection
+    },
+    /*
+     * Projection transformation
+     */
+    transformVector: function(vector, fromProj, toProj) {
+        // Transform the projection of a feature vector or an array of feature
+        // vectors (as used in a collection) between the given projections.
+        if (fromProj.projCode == toProj.projCode) {
+            return vector;
+        }
+        var transformed;
+        if (vector.constructor == Array) {
+            transformed = [];
+            for (var i = 0; i < vector.length; i++) {
+                transformed.push(this.transformVector(vector[i], fromProj, toProj));
+            }
+        } else {
+            var cloned = vector.geometry.clone();
+            transformed = new OpenLayers.Feature.Vector(cloned.transform(fromProj, toProj));
+        }
+        return transformed;
+    },
+    /*
+     * Constructors for layers
+     */
+    wms: {
+        map: function() {
+            return new OpenLayers.Layer.WMS(
+                    "OpenLayers WMS",
+                    "http://labs.metacarta.com/wms/vmap0",
+                    {layers: 'basic'}
+            );
+        },
+        nasa: function() {
+            return new OpenLayers.Layer.WMS(
+                    "NASA Global Mosaic",
+                    "http://t1.hypercube.telascience.org/cgi-bin/landsat7",
+                    {layers: "landsat7"}
+            );
+        },
+        blank: function() {
+            return new OpenLayers.Layer("", {isBaseLayer: true});
+        }
+    },
+    osm: {
+        mapnik: function() {
+            // Not using OpenLayers.Layer.OSM.Mapnik constructor because of
+            // an IE6 bug.  This duplicates that constructor.
+            return new OpenLayers.Layer.OSM("OpenStreetMap (Mapnik)",
+                    [
+                        "http://a.tile.openstreetmap.org/${z}/${x}/${y}.png",
+                        "http://b.tile.openstreetmap.org/${z}/${x}/${y}.png",
+                        "http://c.tile.openstreetmap.org/${z}/${x}/${y}.png"
+                    ],
+                    { numZoomLevels: 19 });
+        },
+        osmarender: function() {
+            return new OpenLayers.Layer.OSM.Osmarender(
+                    'OpenStreetMap (Osmarender)');
+        }
+    },
+    google: {
+        streets: function() {
+            return new OpenLayers.Layer.Google("Google Streets", 
+                    {sphericalMercator: true, numZoomLevels: 20});
+        },
+        physical: function() {
+            return new OpenLayers.Layer.Google("Google Physical", 
+                    {sphericalMercator: true, type: G_PHYSICAL_MAP});
+        },
+        satellite: function() {
+            return new OpenLayers.Layer.Google("Google Satellite", 
+                    {sphericalMercator: true, type: G_SATELLITE_MAP});
+        },
+        hybrid: function() {
+            return new OpenLayers.Layer.Google("Google Hybrid", 
+                    {sphericalMercator: true, type: G_HYBRID_MAP, numZoomLevels: 20});
+        }
+    },
+    yahoo: {
+        map: function() {
+            return new OpenLayers.Layer.Yahoo("Yahoo", 
+                    {sphericalMercator: true, numZoomLevels: 20});
+        }
+    },
+    ve: {
+        map: function(type, typeName) {
+            /* 
+               VE does not play nice with vector layers at zoom level 1.
+               Also, map may need "panMethod: OpenLayers.Easing.Linear.easeOut"
+               to avoid drift.  See:
+
+               http://openlayers.com/dev/examples/ve-novibrate.html
+
+            */
+                
+            return new OpenLayers.Layer.VirtualEarth("Microsoft VE (" + typeName + ")", 
+                {sphericalMercator: true, minZoomLevel: 2, type: type });
+        },
+        road: function() { return this.map(VEMapStyle.Road, "Road"); },
+        shaded: function() { return this.map(VEMapStyle.Shaded, "Shaded"); },
+        aerial: function() { return this.map(VEMapStyle.Aerial, "Aerial"); },
+        hybrid: function() { return this.map(VEMapStyle.Hybrid, "Hybrid"); }
+    },
+
+
+    /*
+     * Deep copies all attributes in 'source' object into 'destination' object.
+     * Useful for applying defaults in nested objects.
+     */
+    deepJoinOptions: function(destination, source) {
+        if (destination == undefined) {
+            destination = {};
+        }
+        for (var a in source) {
+            if (source[a] != undefined) {
+                if (typeof(source[a]) == 'object' && source[a].constructor != Array) {
+                    destination[a] = this.deepJoinOptions(destination[a], source[a]);
+                } else {
+                    destination[a] = source[a];
+                }
+            }
+        }
+        return destination;
+    }
+};
+
+/*
+ *  Base olwidget map type.  Extends OpenLayers.Map
+ */
+olwidget.BaseMap = OpenLayers.Class(OpenLayers.Map, {
+    initialize: function(mapDivID, options) {
+        this.opts = this.initOptions(options);
+        this.initMap(mapDivID, this.opts);
+    },
+    /*
+     * Extend the passed in options with defaults, and create unserialized
+     * objects for serialized options (such as projections).
+     */
+    initOptions: function(options) {
+        var defaults = {
+            mapOptions: {
+                units: 'm',
+                maxResolution: 156543.0339,
+                projection: "EPSG:900913",
+                displayProjection: "EPSG:4326",
+                maxExtent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34],
+                controls: ['LayerSwitcher', 'Navigation', 'PanZoom', 'Attribution']
+            },
+            mapDivClass: '',
+            mapDivStyle: {
+                width: '600px',
+                height: '400px'
+            },
+            name: 'data',
+            layers: ['osm.mapnik'],
+            defaultLon: 0,
+            defaultLat: 0,
+            defaultZoom: 4,
+            overlayStyle: {
+                fillColor: '#ff00ff',
+                strokeColor: '#ff00ff',
+                pointRadius: 6,
+                fillOpacity: 0.5,
+                strokeWidth: 2
+            }
+        };
+        var opts = olwidget.deepJoinOptions(defaults, options);
+
+        opts.overrideZoom = options.defaultZoom != undefined;
+        opts.overrideCenter = options.defaultLat != undefined && options.defaultLon != undefined;
+
+        // construct objects for serialized options
+        var me = opts.mapOptions.maxExtent;
+        opts.mapOptions.maxExtent = new OpenLayers.Bounds(me[0], me[1], me[2], me[3]);
+        opts.mapOptions.projection = new OpenLayers.Projection(opts.mapOptions.projection);
+        opts.mapOptions.displayProjection = new OpenLayers.Projection(
+                opts.mapOptions.displayProjection);
+        opts.default_center = new OpenLayers.LonLat(opts.defaultLon, opts.defaultLat);
+        opts.default_center.transform(opts.mapOptions.displayProjection,
+                opts.mapOptions.projection);
+        var controls = [];
+        for (var i = 0; i < opts.mapOptions.controls.length; i++) {
+            var control = opts.mapOptions.controls[i];
+            controls.push(new OpenLayers.Control[control]());
+        }
+        opts.mapOptions.controls = controls;
+        return opts;
+    },
+    /*
+     * Initialize the OpenLayers Map instance and add layers.
+     */
+    initMap: function(mapDivID, opts) {
+        var mapDiv = document.getElementById(mapDivID);
+        OpenLayers.Util.extend(mapDiv.style, opts.mapDivStyle);
+        mapDiv.className = opts.mapDivClass;
+        var layers = [];
+        for (var i = 0; i < opts.layers.length; i++) {
+            var parts = opts.layers[i].split(".");
+            layers.push(olwidget[parts[0]][parts[1]]());
+            
+            // workaround for problems with Micorsoft layers and vector layer drift
+            // (see http://openlayers.com/dev/examples/ve-novibrate.html)
+            if (parts[0] == "ve") {
+                if (opts.mapOptions.panMethod == undefined) {
+                    opts.mapOptions.panMethod = OpenLayers.Easing.Linear.easeOut;
+                }
+            }
+        }
+        var styleMap = new OpenLayers.StyleMap({'default': new OpenLayers.Style(opts.overlayStyle)});
+
+        // Super constructor
+        OpenLayers.Map.prototype.initialize.apply(this, [mapDiv.id, opts.mapOptions]);
+        this.vectorLayer = new OpenLayers.Layer.Vector(this.opts.name, { styleMap: styleMap });
+        layers.push(this.vectorLayer);
+        this.addLayers(layers);
+        this.setDefaultCenter();
+    },
+    setDefaultCenter: function() {
+        this.setCenter(this.opts.default_center.clone(), this.opts.defaultZoom);
+    },
+    clearFeatures: function() {
+        this.vectorLayer.removeFeatures(this.vectorLayer.features);
+        this.vectorLayer.destroyFeatures();
+    }
+});
+
+/*
+ *  Map type that implements editable vectors.
+ */
+olwidget.EditableMap = OpenLayers.Class(olwidget.BaseMap, {
+    initialize: function(textareaID, options) {
+        var defaults = {
+            editable: true,
+            geometry: 'point',
+            hideTextarea: true,
+            isCollection: false
+        };
+        options = olwidget.deepJoinOptions(defaults, options);
+
+        // set up map div
+        var mapDiv = document.createElement("div");
+        mapDiv.id = textareaID + "_map";
+        this.textarea = document.getElementById(textareaID);
+        this.textarea.parentNode.insertBefore(mapDiv, this.textarea);
+
+        // initialize map
+        olwidget.BaseMap.prototype.initialize.apply(this, [mapDiv.id, options])
+
+        if (this.opts.hideTextarea) {
+            this.textarea.style.display = 'none';
+        }
+
+        this.initWKTAndCenter()
+        this.initControls();
+    },
+    initWKTAndCenter: function() {
+        // Read any initial WKT from the text field.  We assume that the
+        // WKT uses the projection given in "displayProjection", and ignore
+        // any initial SRID.
+
+        var wkt = this.textarea.value;
+        if (wkt) {
+            // After reading into geometry, immediately write back to 
+            // WKT <textarea> as EWKT (so the SRID is included if it wasn't
+            // before).
+            var geom = olwidget.ewktToFeature(wkt);
+            geom = olwidget.transformVector(geom, this.displayProjection, this.projection);
+            if (this.opts.isCollection) {
+                this.vectorLayer.addFeatures(geom);
+            } else {
+                this.vectorLayer.addFeatures([geom]);
+            }
+            this.numGeom = this.vectorLayer.features.length;
+            
+            // Set center
+            if (this.opts.geometry == 'point' && !this.opts.isCollection) {
+                this.setCenter(geom.geometry.getBounds().getCenterLonLat(), 
+                        this.opts.defaultZoom);
+            } else {
+                this.zoomToExtent(this.vectorLayer.getDataExtent());
+            }
+        }
+    },
+    initControls: function() {
+        // Initialize controls for editing geometries, navigating, and map data.
+
+        // This allows editing of the geographic fields -- the modified WKT is
+        // written back to the content field (as EWKT)
+        if (this.opts.editable) {
+            var closuredThis = this;
+            this.vectorLayer.events.on({
+                "featuremodified" : function(event) { closuredThis.modifyWKT(event); },
+                "featureadded": function(event) { closuredThis.addWKT(event); }
+            });
+
+            // Map controls:
+            // Add geometry specific panel of toolbar controls
+            var panel = this.buildPanel(this.vectorLayer);
+            this.addControl(panel);
+            var select = new OpenLayers.Control.SelectFeature( this.vectorLayer,
+                    {toggle: true, clickout: true, hover: false});
+            this.addControl(select);
+            select.activate();
+        }
+    },
+    clearFeatures: function() {
+        olwidget.BaseMap.prototype.clearFeatures.apply(this);
+        this.textarea.value = '';
+    },
+    buildPanel: function(layer) {
+        var panel = new OpenLayers.Control.Panel({displayClass: 'olControlEditingToolbar'});
+        var controls = [];
+
+        var nav = new OpenLayers.Control.Navigation();
+        controls.push(nav);
+
+        // Drawing control(s)
+        var geometries;
+        if (this.opts.geometry.constructor == Array) {
+            geometries = this.opts.geometry; 
+        } else {
+            geometries = [this.opts.geometry];
+        }
+        for (var i = 0; i < geometries.length; i++) {
+            var drawControl;
+            if (geometries[i] == 'linestring') {
+                drawControl = new OpenLayers.Control.DrawFeature(layer, 
+                    OpenLayers.Handler.Path, 
+                    {'displayClass': 'olControlDrawFeaturePath'});
+            } else if (geometries[i] == 'polygon') {
+                drawControl = new OpenLayers.Control.DrawFeature(layer,
+                    OpenLayers.Handler.Polygon,
+                    {'displayClass': 'olControlDrawFeaturePolygon'});
+            } else if (geometries[i] == 'point') {
+                drawControl = new OpenLayers.Control.DrawFeature(layer,
+                    OpenLayers.Handler.Point,
+                    {'displayClass': 'olControlDrawFeaturePoint'});
+            }
+            controls.push(drawControl);
+        }
+            
+        // Modify feature control
+        var mod = new OpenLayers.Control.ModifyFeature(layer);
+        controls.push(mod);
+
+        // Clear all control
+        var closuredThis = this;
+        var del = new OpenLayers.Control.Button({
+            displayClass: 'olControlClearFeatures', 
+            trigger: function() {
+                closuredThis.clearFeatures();
+            }
+        });
+
+        controls.push(del);
+        panel.addControls(controls);
+        return panel;
+    },
+    // Callback for openlayers "featureadded" 
+    addWKT: function(event) {
+        // This function will sync the contents of the `vector` layer with the
+        // WKT in the text field.
+        if (this.opts.isCollection) {
+            this.featureToTextarea(this.vectorLayer.features);
+        } else {
+            // Make sure to remove any previously added features.
+            if (this.vectorLayer.features.length > 1) {
+                old_feats = [this.vectorLayer.features[0]];
+                this.vectorLayer.removeFeatures(old_feats);
+                this.vectorLayer.destroyFeatures(old_feats);
+            }
+            this.featureToTextarea(event.feature);
+        }
+    },
+    // Callback for openlayers "featuremodified" 
+    modifyWKT: function(event) {
+        if (this.opts.isCollection){
+            // OpenLayers adds points around the modified feature that we want to strip.
+            // So only take the features up to "numGeom", the number of features counted
+            // when we last added.
+            var feat = [];
+            for (var i = 0; i < this.numGeom; i++) {
+                feat.push(this.vectorLayer.features[i].clone());
+            }
+            this.featureToTextarea(feat)
+        } else {
+            this.featureToTextarea(event.feature);
+        }
+    },
+    featureToTextarea: function(feature) {
+        if (this.opts.isCollection) {
+            this.numGeom = feature.length;
+        } else {
+            this.numGeom = 1;
+        }
+        feature = olwidget.transformVector(feature, 
+                this.projection, this.displayProjection);
+        if (this.opts.isCollection) {
+            // Convert to multi-geometry types if we are a collection.
+            // Passing arrays to the WKT formatter results in a
+            // "GEOMETRYCOLLECTION" type, but if we have only one geometry,
+            // we should use a "MULTI<geometry>" type.
+            if (this.opts.geometry.constructor != Array) {
+                var geoms = [];
+                for (var i = 0; i < feature.length; i++) {
+                    geoms.push(feature[i].geometry);
+                }
+                var GeoClass = olwidget.multiGeometryClasses[this.opts.geometry]; 
+                feature = new OpenLayers.Feature.Vector(new GeoClass(geoms));
+            } 
+        }
+        this.textarea.value = olwidget.featureToEWKT(feature, this.displayProjection);
+    }
+});
+
+/*
+ *  olwidget.InfoMap -- map type supporting clickable vectors that raise
+ *  popups.  The popups are displayed optionally inside or outside the map's
+ *  viewport.
+ *  
+ *  Usage: olwidget.Infomap(mapDivID, infoArray, options)
+ *
+ *  arguments: 
+ *     mapDivID: the DOM id of a div to replace with the map.
+ *     infoArray: An array of the form:
+ *         [ ["WKT", "html"], ... ]
+ *         where "WKT" represents the well-known text form of the geometry,
+ *         and "html" represents the HTML content for the popup.
+ *     options: An options object.  See distribution documentation for details.
+ */
+olwidget.InfoMap = OpenLayers.Class(olwidget.BaseMap, {
+    initialize: function(mapDivID, infoArray, options) {
+        var infomapDefaults = {
+            popupsOutside: false,
+            popupDirection: 'auto',
+            popupPaginationSeparator: ' of ',
+            cluster: false,
+            clusterDisplay: "paginate",
+            clusterStyle: {
+                pointRadius: "${radius}",
+                strokeWidth: "${width}",
+                label: "${label}",
+                labelSelect: true,
+                fontSize: "11px",
+                fontFamily: "Helvetica, sans-serif",
+                fontColor: "#ffffff"
+            }
+        };
+
+        options = olwidget.deepJoinOptions(infomapDefaults, options);
+        olwidget.BaseMap.prototype.initialize.apply(this, [mapDivID, options]);
+
+        // Must have explicitly specified position for popups to work properly.
+        if (!this.div.style.position) {
+            this.div.style.position = 'relative';
+        }
+
+        if (this.opts.cluster == true) {
+            this.addClusterStrategy();
+        }
+
+        var features = [];
+        for (var i = 0; i < infoArray.length; i++) {
+            var feature = olwidget.ewktToFeature(infoArray[i][0]);
+            feature = olwidget.transformVector(feature, 
+                this.displayProjection, this.projection);
+
+            if (feature.constructor != Array) {
+                feature = [feature];
+            }
+            for (var k = 0; k < feature.length; k++) {
+                feature[k].attributes = { html: infoArray[i][1] };
+                features.push(feature[k]);
+            }
+        }
+        this.vectorLayer.addFeatures(features);
+
+        this.select = new OpenLayers.Control.SelectFeature(this.vectorLayer, { clickout: true, hover: false });
+        this.select.events.register("featurehighlighted", this, 
+                function(evt) { this.createPopup(evt); });
+        this.select.events.register("featureunhighlighted", this, 
+                function(evt) { this.deletePopup() });
+        
+        // Zooming changes clusters, so we must delete popup if we zoom.
+        this.events.register("zoomend", this, function(event) { this.select.unselectAll(); });
+
+        this.addControl(this.select);
+        this.select.activate();
+
+        // Set zoom level
+        if (this.opts.overrideCenter) {
+            this.setCenter(this.opts.default_center);
+        } else {
+            this.setCenter(this.vectorLayer.getDataExtent().getCenterLonLat());
+        }
+        if (this.opts.overrideZoom) {
+            this.zoomTo(this.opts.defaultZoom);
+        } else {
+            this.zoomToExtent(this.vectorLayer.getDataExtent());
+        }
+    },
+    addClusterStrategy: function() {
+        var style_context = {
+            width: function(feature) {
+                return (feature.cluster) ? 2 : 1;
+            },
+            radius: function(feature) {
+                var n = feature.attributes.count;
+                var pix;
+                if (n == 1) {
+                    pix = 6;
+                } else if (n <= 5) {
+                    pix = 8;
+                } else if (n <= 25) {
+                    pix = 10;
+                } else if (n <= 50) {
+                    pix = 12;
+                } else {
+                    pix = 14;
+                }
+                return pix;
+            },
+            label: function(feature) {
+                return (feature.cluster && feature.cluster.length > 1) ? feature.cluster.length : '';
+            }
+        };
+
+        var defaultStyleOpts = OpenLayers.Util.applyDefaults(
+            OpenLayers.Util.applyDefaults({}, this.opts.clusterStyle), 
+            this.vectorLayer.styleMap.styles['default'].defaultStyle);
+        var selectStyleOpts = OpenLayers.Util.applyDefaults(
+            OpenLayers.Util.applyDefaults({}, this.opts.clusterStyle),
+            this.vectorLayer.styleMap.styles['select'].defaultStyle);
+
+        var defaultStyle = new OpenLayers.Style(defaultStyleOpts, {context: style_context});
+        var selectStyle = new OpenLayers.Style(selectStyleOpts, {context: style_context});
+        this.removeLayer(this.vectorLayer);
+        this.vectorLayer = new OpenLayers.Layer.Vector(this.opts.name, { 
+            styleMap: new OpenLayers.StyleMap({ 'default': defaultStyle, 'select': selectStyle }),
+            strategies: [new OpenLayers.Strategy.Cluster()]
+        });
+        this.addLayer(this.vectorLayer);
+    },
+    /**
+     * Override parent to allow placement of popups outside viewport
+     */
+    addPopup: function(popup, exclusive) {
+        if (exclusive) {
+            //remove all other popups from screen
+            for (var i = this.popups.length - 1; i >= 0; --i) {
+                this.removePopup(this.popups[i]);
+            }
+        }
+
+        popup.map = this;
+        this.popups.push(popup);
+        var popupDiv = popup.draw();
+        if (popupDiv) {
+            popupDiv.style.zIndex = this.Z_INDEX_BASE['Popup'] +
+                                    this.popups.length;
+            //if (this.opts.popupsOutside) {
+                this.div.appendChild(popupDiv);
+                // store a reference to this function so we can unregister on removal
+                this.popupMoveFunc = function(event) {
+                    var px = this.getPixelFromLonLat(this.popup.lonlat);
+                    popup.moveTo(px);
+                }
+                this.events.register("move", this, this.popupMoveFunc);
+                this.popupMoveFunc();
+            //} else {
+            //    this.layerContainerDiv.appendChild(popupDiv);
+            //}
+        }
+    },
+    /**
+     * Override parent to allow placement of popups outside viewport
+     */
+    removePopup: function(popup) {
+        OpenLayers.Util.removeItem(this.popups, popup);
+        if (popup.div) {
+            try {
+                //if (this.opts.popupsOutside) {
+                    this.div.removeChild(popup.div);
+                    this.events.unregister("move", this, this.popupMoveFunc);
+                //} else {
+                //    this.layerContainerDiv.removeChild(popup.div);
+                //}
+            } catch (e) { }
+        }
+        popup.map = null;
+    },
+
+    /**
+     * Build a paginated popup
+     */
+    createPopup: function(evt) {
+        var feature = evt.feature;
+        var lonlat;
+        if (feature.geometry.CLASS_NAME == "OpenLayers.Geometry.Point") {
+            lonlat = feature.geometry.getBounds().getCenterLonLat();
+        } else {
+            lonlat = this.getLonLatFromViewPortPx(evt.object.handlers.feature.evt.xy);
+        }
+            
+        var popupHTML = [];
+        if (feature.cluster) {
+            if (this.opts.clusterDisplay == 'list') {
+                if (feature.cluster.length > 1) {
+                    var html = "<ul class='olwidgetClusterList'>";
+                    for (var i = 0; i < feature.cluster.length; i++) {
+                        html += "<li>" + feature.cluster[i].attributes.html + "</li>";
+                    }
+                    html += "</ul>";
+                    popupHTML.push(html);
+                } else {
+                    popupHTML.push(feature.cluster[0].attributes.html);
+                }
+            } else {
+                for (var i = 0; i < feature.cluster.length; i++) {
+                    popupHTML.push(feature.cluster[i].attributes.html);
+                }
+            }
+        } else {
+            popupHTML.push(feature.attributes.html);
+        }
+        var infomap = this;
+        this.popup = new olwidget.Popup(null, 
+                lonlat, null, popupHTML, null, true, 
+                function() { infomap.select.unselect(feature) }, 
+                this.opts.popupDirection,
+                this.opts.popupPaginationSeparator);
+        if (this.opts.popupsOutside) {
+            this.popup.panMapIfOutOfView = false;
+        }
+        this.addPopup(this.popup);
+    },
+
+    deletePopup: function() {
+        if (this.popup) {
+            this.popup.destroy();
+            this.popup = null;
+        }
+    }
+});
+
+/*
+ * Paginated, framed popup type, CSS stylable.
+ */
+olwidget.Popup = OpenLayers.Class(OpenLayers.Popup.Framed, {
+    autoSize: true,
+    panMapIfOutOfView: true,
+    fixedRelativePosition: false,
+    // Position blocks.  Overriden to include additional "className" parameter,
+    // allowing image paths relative to css rather than relative to the html
+    // file (as paths included in a JS file are computed).
+    positionBlocks: {
+        "tl": {
+            'offset': new OpenLayers.Pixel(44, -6),
+            'padding': new OpenLayers.Bounds(5, 14, 5, 5),
+            'blocks': [
+                { // stem
+                    className: 'olwidgetPopupStemTL',
+                    size: new OpenLayers.Size(24, 14),
+                    anchor: new OpenLayers.Bounds(null, 0, 32, null),
+                    position: new OpenLayers.Pixel(0, -28)
+                }
+            ]
+        },
+        "tr": {
+            'offset': new OpenLayers.Pixel(-44, -6),
+            'padding': new OpenLayers.Bounds(5, 14, 5, 5),
+            'blocks': [
+                { // stem
+                    className: "olwidgetPopupStemTR",
+                    size: new OpenLayers.Size(24, 14),
+                    anchor: new OpenLayers.Bounds(32, 0, null, null),
+                    position: new OpenLayers.Pixel(0, -28)
+                }
+            ]
+        },
+        "bl": {
+            'offset': new OpenLayers.Pixel(44, 6),
+            'padding': new OpenLayers.Bounds(5, 5, 5, 14),
+            'blocks': [
+                { // stem
+                    className: "olwidgetPopupStemBL",
+                    size: new OpenLayers.Size(24, 14),
+                    anchor: new OpenLayers.Bounds(null, null, 32, 0),
+                    position: new OpenLayers.Pixel(0, 0)
+                }
+            ]
+        },
+        "br": {
+            'offset': new OpenLayers.Pixel(-44, 6),
+            'padding': new OpenLayers.Bounds(5, 5, 5, 14),
+            'blocks': [
+                { // stem
+                    className: "olwidgetPopupStemBR",
+                    size: new OpenLayers.Size(24, 14),
+                    anchor: new OpenLayers.Bounds(32, null, null, 0),
+                    position: new OpenLayers.Pixel(0, 0)
+                }
+            ]
+        }
+    },
+
+    initialize: function(id, lonlat, contentSize, contentHTML, anchor, closeBox,
+                    closeBoxCallback, relativePosition, separator) {
+        if (relativePosition && relativePosition != 'auto') {
+            this.fixedRelativePosition = true;
+            this.relativePosition = relativePosition;
+        }
+        if (separator == undefined) {
+            this.separator = ' of ';
+        } else {
+            this.separator = separator;
+        }
+        // we don't use the default close box because we want it to appear in
+        // the content div for easier CSS control.
+        this.olwidgetCloseBox = closeBox;
+        this.olwidgetCloseBoxCallback = closeBoxCallback;
+        this.page = 0;
+        OpenLayers.Popup.Framed.prototype.initialize.apply(this, [id, lonlat,
+            contentSize, contentHTML, anchor, false, null]);
+    },
+
+    /*
+     * Construct the interior of a popup.  If contentHTML is an Array, display
+     * the array element specified by this.page.
+     */
+    setContentHTML: function(contentHTML) {
+        if (contentHTML != null) {
+            this.contentHTML = contentHTML;
+        }
+
+        var pageHTML;
+        var showPagination;
+        if (this.contentHTML.constructor != Array) {
+            pageHTML = this.contentHTML;
+            showPagination = false;
+        } else {
+            pageHTML = this.contentHTML[this.page];
+            showPagination = this.contentHTML.length > 1;
+        }
+
+        if ((this.contentDiv != null) && (pageHTML != null)) {
+            var popup = this; // for closures
+
+            // Clear old contents
+            this.contentDiv.innerHTML = "";
+            
+            // Build container div
+            var containerDiv = document.createElement("div");
+            containerDiv.className = 'olwidgetPopupContent';
+            this.contentDiv.appendChild(containerDiv);
+
+            // Build close box
+            if (this.olwidgetCloseBox) {
+                closeDiv = document.createElement("div");
+                closeDiv.className = "olwidgetPopupCloseBox";
+                closeDiv.innerHTML = "close";
+                closeDiv.onclick = function(event) { 
+                    popup.olwidgetCloseBoxCallback.apply(popup, arguments); 
+                }
+                containerDiv.appendChild(closeDiv);
+            }
+            
+            var pageDiv = document.createElement("div");
+            pageDiv.innerHTML = pageHTML;
+            pageDiv.className = "olwidgetPopupPage";
+            containerDiv.appendChild(pageDiv);
+
+            if (showPagination) {
+                // Build pagination control
+
+                paginationDiv = document.createElement("div");
+                paginationDiv.className = "olwidgetPopupPagination";
+                var prev = document.createElement("div");
+                prev.className = "olwidgetPaginationPrevious";
+                prev.innerHTML = "prev";
+                prev.onclick = function(event) { 
+                    popup.page = (popup.page - 1 + popup.contentHTML.length) % 
+                        popup.contentHTML.length;
+                    popup.setContentHTML();
+                    popup.map.events.triggerEvent("move");
+                }
+
+                var count = document.createElement("div");
+                count.className = "olwidgetPaginationCount";
+                count.innerHTML = (this.page + 1) + " of " + this.contentHTML.length;
+                var next = document.createElement("div");
+                next.className = "olwidgetPaginationNext";
+                next.innerHTML = "next";
+                next.onclick = function(event) {
+                    popup.page = (popup.page + 1) % popup.contentHTML.length;
+                    popup.setContentHTML();
+                    popup.map.events.triggerEvent("move");
+                }
+
+                paginationDiv.appendChild(prev);
+                paginationDiv.appendChild(count);
+                paginationDiv.appendChild(next);
+                containerDiv.appendChild(paginationDiv);
+
+            }
+            var clearFloat = document.createElement("div");
+            clearFloat.style.clear = "both";
+            containerDiv.appendChild(clearFloat);
+
+            if (this.autoSize) {
+                this.registerImageListeners();
+                this.updateSize();
+            }
+        }
+    },
+
+    /*
+     * Override parent to make the popup more CSS-friendly.  Rather than
+     * specifying img paths in javascript, give position blocks CSS classes
+     * that can be used to apply background images to the divs.
+     */
+    createBlocks: function() {
+        this.blocks = [];
+
+        //since all positions contain the same number of blocks, we can 
+        // just pick the first position and use its blocks array to create
+        // our blocks array
+        var firstPosition = null;
+        for(var key in this.positionBlocks) {
+            firstPosition = key;
+            break;
+        }
+
+        var position = this.positionBlocks[firstPosition];
+        for (var i = 0; i < position.blocks.length; i++) {
+
+            var block = {};
+            this.blocks.push(block);
+
+            var divId = this.id + '_FrameDecorationDiv_' + i;
+            block.div = OpenLayers.Util.createDiv(divId,
+                null, null, null, "absolute", null, "hidden", null
+            );
+            this.groupDiv.appendChild(block.div);
+        }
+    },
+    /*
+     * Override parent to make the popup more CSS-friendly, reflecting
+     * modifications to createBlocks.
+     */
+    updateBlocks: function() {
+        if (!this.blocks) {
+            this.createBlocks();
+        }
+        if (this.size && this.relativePosition) {
+            var position = this.positionBlocks[this.relativePosition];
+            for (var i = 0; i < position.blocks.length; i++) {
+
+                var positionBlock = position.blocks[i];
+                var block = this.blocks[i];
+
+                // adjust sizes
+                var l = positionBlock.anchor.left;
+                var b = positionBlock.anchor.bottom;
+                var r = positionBlock.anchor.right;
+                var t = positionBlock.anchor.top;
+
+                //note that we use the isNaN() test here because if the 
+                // size object is initialized with a "auto" parameter, the 
+                // size constructor calls parseFloat() on the string, 
+                // which will turn it into NaN
+                //
+                var w = (isNaN(positionBlock.size.w)) ? this.size.w - (r + l)
+                                                      : positionBlock.size.w;
+
+                var h = (isNaN(positionBlock.size.h)) ? this.size.h - (b + t)
+                                                      : positionBlock.size.h;
+
+                block.div.style.width = (w < 0 ? 0 : w) + 'px';
+                block.div.style.height = (h < 0 ? 0 : h) + 'px';
+
+                block.div.style.left = (l != null) ? l + 'px' : '';
+                block.div.style.bottom = (b != null) ? b + 'px' : '';
+                block.div.style.right = (r != null) ? r + 'px' : '';
+                block.div.style.top = (t != null) ? t + 'px' : '';
+
+                block.div.className = positionBlock.className;
+            }
+
+            this.contentDiv.style.left = this.padding.left + "px";
+            this.contentDiv.style.top = this.padding.top + "px";
+        }
+    },
+    updateSize: function() {
+        if (this.map.opts.popupsOutside == true) {
+            var preparedHTML = "<div class='" + this.contentDisplayClass+ "'>" +
+                this.contentDiv.innerHTML +
+                "</div>";
+
+            var containerElement = document.body;
+            var realSize = OpenLayers.Util.getRenderedDimensions(
+                preparedHTML, null, {
+                    displayClass: this.displayClass,
+                    containerElement: containerElement
+                }
+            );
+            return this.setSize(realSize);
+        } else {
+            return OpenLayers.Popup.prototype.updateSize.apply(this, arguments);
+        }
+    },
+
+    CLASS_NAME: "olwidget.Popup"
+});
+
+// finish anonymous function.  Add olwidget to 'window'.
+this.olwidget = olwidget;
+})();
Index: contrib/admin/options.py
===================================================================
--- contrib/admin/options.py	(revision 11585)
+++ contrib/admin/options.py	(working copy)
@@ -873,8 +873,8 @@
         return self.render_change_form(request, context, change=True, obj=obj)
     change_view = transaction.commit_on_success(change_view)
 
-    def changelist_view(self, request, extra_context=None):
-        "The 'change list' admin view for this model."
+    def get_changelist_context(self, request):
+        "Get the default context for the changelist view."
         from django.contrib.admin.views.main import ChangeList, ERROR_FLAG
         opts = self.model._meta
         app_label = opts.app_label
@@ -977,6 +977,12 @@
             'actions_on_top': self.actions_on_top,
             'actions_on_bottom': self.actions_on_bottom,
         }
+        return context
+
+    def changelist_view(self, request, extra_context=None):
+        "The 'change list' admin view for this model."
+
+        context = self.get_changelist_context(request)
         context.update(extra_context or {})
         context_instance = template.RequestContext(request, current_app=self.admin_site.name)
         return render_to_response(self.change_list_template or [
