[ckan-changes] [okfn/ckan] 687b36: [2275][model, schema, tests] Add logo field to gro...

GitHub noreply at github.com
Mon Apr 16 16:40:00 UTC 2012


  Branch: refs/heads/master
  Home:   https://github.com/okfn/ckan
  Commit: 687b360051ec08e88e60674b72c22b8d1f5217ba
      https://github.com/okfn/ckan/commit/687b360051ec08e88e60674b72c22b8d1f5217ba
  Author: John Glover <j at johnglover.net>
  Date:   2012-04-11 (Wed, 11 Apr 2012)

  Changed paths:
    M ckan/logic/schema.py
    A ckan/migration/versions/052_add_group_logo.py
    M ckan/model/group.py
    M ckan/tests/lib/test_dictization.py
    M ckan/tests/lib/test_dictization_schema.py

  Log Message:
  -----------
  [2275][model, schema, tests] Add logo field to groups


diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py
index e77b894..aabf757 100644
--- a/ckan/logic/schema.py
+++ b/ckan/logic/schema.py
@@ -179,6 +179,7 @@ def default_group_schema():
         'name': [not_empty, unicode, name_validator, group_name_validator],
         'title': [ignore_missing, unicode],
         'description': [ignore_missing, unicode],
+        'logo': [ignore_missing, unicode],
         'type': [ignore_missing, unicode],
         'state': [ignore_not_group_admin, ignore_missing],
         'created': [ignore],
diff --git a/ckan/migration/versions/052_add_group_logo.py b/ckan/migration/versions/052_add_group_logo.py
new file mode 100644
index 0000000..4f0a0d4
--- /dev/null
+++ b/ckan/migration/versions/052_add_group_logo.py
@@ -0,0 +1,12 @@
+from sqlalchemy import *
+from migrate import *
+
+def upgrade(migrate_engine):
+    migrate_engine.execute('''
+        ALTER TABLE "group"
+            ADD COLUMN logo text;
+
+        ALTER TABLE group_revision
+            ADD COLUMN logo text;
+    '''
+    )
diff --git a/ckan/model/group.py b/ckan/model/group.py
index b43ee10..78eae15 100644
--- a/ckan/model/group.py
+++ b/ckan/model/group.py
@@ -31,6 +31,7 @@
     Column('title', UnicodeText),
     Column('type', UnicodeText, nullable=False),
     Column('description', UnicodeText),
+    Column('logo', UnicodeText),
     Column('created', DateTime, default=datetime.datetime.now),
     Column('approval_status', UnicodeText, default=u"approved"),
     )
@@ -78,11 +79,12 @@ class Group(vdm.sqlalchemy.RevisionedObjectMixin,
             vdm.sqlalchemy.StatefulObjectMixin,
             DomainObject):
 
-    def __init__(self, name=u'', title=u'', description=u'',
-                 type=u'group', approval_status=u'approved' ):
+    def __init__(self, name=u'', title=u'', description=u'', logo=u'',
+                 type=u'group', approval_status=u'approved'):
         self.name = name
         self.title = title
         self.description = description
+        self.logo = logo
         self.type = type
         self.approval_status= approval_status
 
diff --git a/ckan/tests/lib/test_dictization.py b/ckan/tests/lib/test_dictization.py
index ba8ec69..e6e8ae3 100644
--- a/ckan/tests/lib/test_dictization.py
+++ b/ckan/tests/lib/test_dictization.py
@@ -42,19 +42,19 @@ def setup_class(cls):
             'groups': [{'description': u'These are books that David likes.',
                         'name': u'david',
                         'capacity': 'public',
+                        'logo': u'',
                         'type': u'group',
                         'state': u'active',
                         'title': u"Dave's books",
-                        "approval_status": u"approved",
-                        'capacity': u'public'},
+                        "approval_status": u"approved"},
                        {'description': u'Roger likes these books.',
                         'name': u'roger',
                         'capacity': 'public',
+                        'logo': u'',
                         'type': u'group',
                         'state': u'active',
                         'title': u"Roger's books",
-                        "approval_status": u"approved",
-                        'capacity': u'public'}],
+                        "approval_status": u"approved"}],
             'isopen': True,
             'license_id': u'other-open',
             'license_title': u'Other (Open)',
@@ -212,7 +212,7 @@ def test_01_dictize_main_objects_simple(self):
     def test_02_package_dictize(self):
 
         context = {"model": model,
-                 "session": model.Session}
+                   "session": model.Session}
 
         model.Session.remove()
         pkg = model.Session.query(model.Package).filter_by(name='annakarenina').first()
@@ -864,13 +864,14 @@ def test_16_group_dictized(self):
 
         group_dictized = group_dictize(group, context)
 
-        expected =  {'description': u'',
+        expected = {'description': u'',
                     'extras': [{'key': u'genre', 'state': u'active', 'value': u'"horror"'},
                                {'key': u'media', 'state': u'active', 'value': u'"dvd"'}],
                     'tags': [{'capacity': 'public', 'name': u'russian'}],
                     'groups': [{'description': u'',
                                'capacity' : 'public',
                                'display_name': u'simple',
+                               'logo': u'',
                                'name': u'simple',
                                'packages': 0,
                                'state': u'active',
@@ -889,6 +890,7 @@ def test_16_group_dictized(self):
                               'reset_key': None}],
                     'name': u'help',
                     'display_name': u'help',
+                    'logo': u'',
                     'packages': [{'author': None,
                                   'author_email': None,
                                   'license_id': u'other-open',
diff --git a/ckan/tests/lib/test_dictization_schema.py b/ckan/tests/lib/test_dictization_schema.py
index 13f14e1..f0cf8e7 100644
--- a/ckan/tests/lib/test_dictization_schema.py
+++ b/ckan/tests/lib/test_dictization_schema.py
@@ -149,6 +149,7 @@ def test_2_group_schema(self):
                                  'id': group.id,
                                  'name': u'david',
                                  'type': u'group',
+                                 'logo': u'',
                                  'packages': sorted([{'id': group_pack[0].id,
                                                     'name': group_pack[0].name,
                                                     'title': group_pack[0].title},


================================================================
  Commit: d779c59e0ebaafb2f9d0c3bfc99191399132ea3f
      https://github.com/okfn/ckan/commit/d779c59e0ebaafb2f9d0c3bfc99191399132ea3f
  Author: John Glover <j at johnglover.net>
  Date:   2012-04-11 (Wed, 11 Apr 2012)

  Changed paths:
    M ckan/templates/group/new_group_form.html

  Log Message:
  -----------
  [2275] add logo field to group edit form


diff --git a/ckan/templates/group/new_group_form.html b/ckan/templates/group/new_group_form.html
index ad24ea3..c56239a 100644
--- a/ckan/templates/group/new_group_form.html
+++ b/ckan/templates/group/new_group_form.html
@@ -43,6 +43,13 @@
       ${markdown_editor('description', data.get('description'), 'notes', _('Start with a summary sentence ...'))}
     </div>
   </div>
+  <div class="control-group">
+    <label for="name" class="control-label">Logo URL:</label>
+    <div class="controls">
+      <input id="logo" name="logo" type="text" value="${data.get('logo', '')}"/>
+      <p>The URL for the logo that is associated with this group.</p>
+    </div>
+  </div>
   <div class="state-field control-group" py:if="c.is_sysadmin or c.auth_for_change_state">
     <label for="" class="control-label">State</label>
     <div class="controls">


================================================================
  Commit: af69fd69472abdb3187f19e7f12d37756ed7ce66
      https://github.com/okfn/ckan/commit/af69fd69472abdb3187f19e7f12d37756ed7ce66
  Author: John Glover <j at johnglover.net>
  Date:   2012-04-11 (Wed, 11 Apr 2012)

  Changed paths:
    M ckan/public/css/style.css
    M ckan/templates/group/read.html
    M ckan/templates/layout_base.html

  Log Message:
  -----------
  [2275] add logo to group read page


diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css
index 354734e..981156e 100644
--- a/ckan/public/css/style.css
+++ b/ckan/public/css/style.css
@@ -196,6 +196,11 @@ tbody tr:nth-child(odd) td, tbody tr.odd td {
   font-size: 2.2em;
   font-weight: normal;
 }
+#page-logo {
+  max-width: 36px;
+  max-height: 36px;
+  margin-right: 5px;
+}
 .hover-for-help {
   position: relative;
 }
diff --git a/ckan/templates/group/read.html b/ckan/templates/group/read.html
index 69314b4..5cf7bb5 100644
--- a/ckan/templates/group/read.html
+++ b/ckan/templates/group/read.html
@@ -6,6 +6,9 @@
   <xi:include href="../facets.html" />
   <py:def function="page_title">${c.group.display_name}</py:def>
   <py:def function="page_heading">${c.group.display_name}</py:def>
+  <py:if test="c.group.logo">
+    <py:def function="page_logo">${c.group.logo}</py:def>
+  </py:if>
 
   <py:match path="primarysidebar">
   
diff --git a/ckan/templates/layout_base.html b/ckan/templates/layout_base.html
index 3b090f1..1518d72 100644
--- a/ckan/templates/layout_base.html
+++ b/ckan/templates/layout_base.html
@@ -90,7 +90,10 @@
     </py:with>
 
     <div id="main" class="container" role="main">
-      <h1 py:if="defined('page_heading')" class="page_heading">${page_heading()}</h1>
+      <h1 py:if="defined('page_heading')" class="page_heading">
+        <img py:if="defined('page_logo')" id="page-logo" src="${page_logo()}" alt="Page Logo" />
+        ${page_heading()}
+      </h1>
       <div class="row">
         <div class="span12">
           <div id="minornavigation">


================================================================
  Commit: 9424ea0fd5f6ae41bd9ab85d782dd8a956b6c4a9
      https://github.com/okfn/ckan/commit/9424ea0fd5f6ae41bd9ab85d782dd8a956b6c4a9
  Author: John Glover <j at johnglover.net>
  Date:   2012-04-11 (Wed, 11 Apr 2012)

  Changed paths:
    M ckan/tests/functional/test_group.py

  Log Message:
  -----------
  [2275][tests] add test for editing a group and saving a logo URL


diff --git a/ckan/tests/functional/test_group.py b/ckan/tests/functional/test_group.py
index 8b452a6..24a30d1 100644
--- a/ckan/tests/functional/test_group.py
+++ b/ckan/tests/functional/test_group.py
@@ -259,6 +259,20 @@ def test_edit_plugin_hook(self):
         assert plugin.calls['edit'] == 1, plugin.calls
         plugins.unload(plugin)
 
+    def test_edit_logo(self):
+        group = model.Group.by_name(self.groupname)
+        offset = url_for(controller='group', action='edit', id=self.groupname)
+        res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'})
+
+        form = res.forms['group-edit']
+        logo_url = u'http://url.to/logo'
+        form['logo'] = logo_url
+        res = form.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'})
+
+        model.Session.remove()
+        group = model.Group.by_name(self.groupname)
+        assert group.logo == logo_url, group
+
     def test_edit_non_existent(self):
         name = u'group_does_not_exist'
         offset = url_for(controller='group', action='edit', id=name)


================================================================
  Commit: 75d75bc05fbb7f457ecbbe644636709ecb22abb6
      https://github.com/okfn/ckan/commit/75d75bc05fbb7f457ecbbe644636709ecb22abb6
  Author: John Glover <j at johnglover.net>
  Date:   2012-04-11 (Wed, 11 Apr 2012)

  Changed paths:
    M ckan/tests/functional/test_group.py

  Log Message:
  -----------
  [2275][tests] update group test for changes to main layout (due to adding group logo)


diff --git a/ckan/tests/functional/test_group.py b/ckan/tests/functional/test_group.py
index 24a30d1..c629cbd 100644
--- a/ckan/tests/functional/test_group.py
+++ b/ckan/tests/functional/test_group.py
@@ -72,7 +72,7 @@ def test_mainmenu(self):
     def test_index(self):
         offset = url_for(controller='group', action='index')
         res = self.app.get(offset)
-        assert '<h1 class="page_heading">Groups' in res, res
+        assert re.search('<h1(.*)>\s*Groups', res.body)
         groupname = 'david'
         group = model.Group.by_name(unicode(groupname))
         group_title = group.title


================================================================
  Commit: c70c3ec82b130287e4d507d429ff6e4622793493
      https://github.com/okfn/ckan/commit/c70c3ec82b130287e4d507d429ff6e4622793493
  Author: John Glover <j at johnglover.net>
  Date:   2012-04-16 (Mon, 16 Apr 2012)

  Changed paths:
    M ckan/controllers/storage.py
    M ckan/forms/authorization_group.py
    M ckan/lib/base.py
    M ckan/lib/dictization/model_save.py
    M ckan/lib/helpers.py
    M ckan/lib/helpers_clean.py
    M ckan/lib/navl/dictization_functions.py
    M ckan/lib/navl/validators.py
    A ckan/migration/versions/052_update_member_capacities.py
    M ckan/model/license.py
    M ckan/public/scripts/application.js
    M ckan/public/scripts/templates.js
    A ckan/public/scripts/vendor/leaflet/0.3.1/images/layers.png
    A ckan/public/scripts/vendor/leaflet/0.3.1/images/marker-shadow.png
    A ckan/public/scripts/vendor/leaflet/0.3.1/images/marker.png
    A ckan/public/scripts/vendor/leaflet/0.3.1/images/popup-close.png
    A ckan/public/scripts/vendor/leaflet/0.3.1/images/zoom-in.png
    A ckan/public/scripts/vendor/leaflet/0.3.1/images/zoom-out.png
    A ckan/public/scripts/vendor/leaflet/0.3.1/leaflet.css
    A ckan/public/scripts/vendor/leaflet/0.3.1/leaflet.ie.css
    A ckan/public/scripts/vendor/leaflet/0.3.1/leaflet.js
    M ckan/public/scripts/vendor/recline/css/data-explorer.css
    A ckan/public/scripts/vendor/recline/css/map.css
    M ckan/public/scripts/vendor/recline/recline.js
    M ckan/templates/_snippet/data-api-help.html
    M ckan/templates/_util.html
    M ckan/templates/js_strings.html
    M ckan/templates/package/history.html
    M ckan/templates/package/resource_read.html
    A ckan/templates/snippets/package_list.html
    A ckan/templates/snippets/revision_list.html
    M ckan/tests/functional/test_admin.py
    M ckan/tests/functional/test_storage.py
    A doc/geospatial.rst
    M doc/index.rst

  Log Message:
  -----------
  Merge remote-tracking branch 'origin/master' into feature-2275-group-logo


diff --git a/ckan/controllers/storage.py b/ckan/controllers/storage.py
index 34af131..594d915 100644
--- a/ckan/controllers/storage.py
+++ b/ckan/controllers/storage.py
@@ -115,7 +115,7 @@ def ofs(self):
     def upload(self):
         label = key_prefix + request.params.get('filepath', str(uuid.uuid4()))
         c.data = {
-            'action': h.url_for('storage_upload_handle'),
+            'action': h.url_for('storage_upload_handle', qualified=False),
             'fields': [
                 {
                     'name': 'key',
@@ -266,7 +266,7 @@ def get_metadata(self, label):
         else:
             url = h.url_for('storage_file',
                     label=label,
-                    qualified=True
+                    qualified=False
                     )
         if not self.ofs.exists(bucket, label):
             abort(404)
@@ -372,7 +372,7 @@ def _get_form_data(self, label):
             return self._get_remote_form_data(label)
         else:
             data = {
-                'action': h.url_for('storage_upload_handle', qualified=True),
+                'action': h.url_for('storage_upload_handle', qualified=False),
                 'fields': [
                     {
                         'name': 'key',
diff --git a/ckan/forms/authorization_group.py b/ckan/forms/authorization_group.py
index ff9fc35..a8df69e 100644
--- a/ckan/forms/authorization_group.py
+++ b/ckan/forms/authorization_group.py
@@ -42,9 +42,10 @@ def render(self, **kwargs):
 
 def build_authorization_group_form(is_admin=False, with_users=False):
     builder = FormBuilder(model.AuthorizationGroup)
-    builder.set_field_text('name', _('Name'), 
-            literal(_("%sUnique identifier%s for group. %s2+ chars, lowercase, using only 'a-z0-9' and '-_'"
-                % ('<br/><strong>', '</strong>', '<br/>'))))
+    builder.set_field_text('name', _('Name'), literal(
+         '<br/>' + _("<strong>Unique identifier</strong> for group.") +
+         '<br/>' + _("2+ characters, lowercase, using only 'a-z0-9' and '-_'")
+        ))
     builder.set_field_option('name', 'validate', common.group_name_validator)
     builder.set_field_option('name', 'required')
     displayed_fields = ['name']
diff --git a/ckan/lib/base.py b/ckan/lib/base.py
index f059956..5fa12bf 100644
--- a/ckan/lib/base.py
+++ b/ckan/lib/base.py
@@ -188,20 +188,23 @@ def __call__(self, environ, start_response):
         # This also improves the cachability of our pages as cookies
         # prevent proxy servers from caching content unless they have
         # been configured to ignore them.
-        for cookie in request.cookies:
-            if cookie.startswith('ckan') and cookie not in ['ckan']:
-                response.delete_cookie(cookie)
-            # Remove the ckan session cookie if not used e.g. logged out
-            elif cookie == 'ckan' and not c.user and not h.are_there_flash_messages():
-                if session.id:
-                    if not session.get('lang'):
-                        session.delete()
-                else:
-                    response.delete_cookie(cookie)
-            # Remove auth_tkt repoze.who cookie if user not logged in.
-            elif cookie == 'auth_tkt' and not session.id:
-                response.delete_cookie(cookie)
 
+        # we need to be careful with the /user/set_lang/ URL as this
+        # creates a cookie.
+        if not environ.get('HTTP_PATH', '').startswith('/user/set_lang/'):
+            for cookie in request.cookies:
+                if cookie.startswith('ckan') and cookie not in ['ckan']:
+                    response.delete_cookie(cookie)
+                # Remove the ckan session cookie if not used e.g. logged out
+                elif cookie == 'ckan' and not c.user and not h.are_there_flash_messages():
+                    if session.id:
+                        if not session.get('lang'):
+                            session.delete()
+                    else:
+                        response.delete_cookie(cookie)
+                # Remove auth_tkt repoze.who cookie if user not logged in.
+                elif cookie == 'auth_tkt' and not session.id:
+                    response.delete_cookie(cookie)
         try:
             return WSGIController.__call__(self, environ, start_response)
         finally:
diff --git a/ckan/lib/dictization/model_save.py b/ckan/lib/dictization/model_save.py
index d4e022a..e8b2658 100644
--- a/ckan/lib/dictization/model_save.py
+++ b/ckan/lib/dictization/model_save.py
@@ -213,7 +213,7 @@ def package_membership_list_save(group_dicts, package, context):
     for group_dict in group_dicts:
         id = group_dict.get("id")
         name = group_dict.get("name")
-        capacity = group_dict.get("capacity", "member")
+        capacity = group_dict.get("capacity", "public")
         if id:
             group = session.query(model.Group).get(id)
         else:
diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py
index cf5373b..7996e7f 100644
--- a/ckan/lib/helpers.py
+++ b/ckan/lib/helpers.py
@@ -31,6 +31,9 @@
 from pylons import session
 from pylons import c
 from pylons.i18n import _
+from pylons.templating import pylons_globals
+from genshi.template import MarkupTemplate
+from ckan.plugins import PluginImplementations, IGenshiStreamFilter
 
 get_available_locales = i18n.get_available_locales
 get_locales_dict = i18n.get_locales_dict
@@ -460,10 +463,10 @@ def format_icon(_format):
     return 'page_white'
 
 def linked_gravatar(email_hash, size=100, default=None):
-    return literal('''<a href="https://gravatar.com/" target="_blank"
-        title="Update your avatar at gravatar.com">
-        %s</a>''' %
-            gravatar(email_hash,size,default)
+    return literal(
+        '<a href="https://gravatar.com/" target="_blank"' +
+        'title="%s">' % _('Update your avatar at gravatar.com') +
+        '%s</a>' % gravatar(email_hash,size,default)
         )
 
 _VALID_GRAVATAR_DEFAULTS = ['404', 'mm', 'identicon', 'monsterid', 'wavatar', 'retro']
@@ -646,3 +649,53 @@ def activity_div(template, activity, actor, object=None, target=None):
     template = template.format(actor=actor, date=date, object=object, target=target)
     template = '<div class="activity">%s %s</div>' % (template, date)
     return literal(template)
+
+def snippet(template_name, **kw):
+    ''' This function is used to load html snippets into pages. keywords
+    can be used to pass parameters into the snippet rendering '''
+    pylons_globs = pylons_globals()
+    genshi_loader = pylons_globs['app_globals'].genshi_loader
+    template = genshi_loader.load(template_name, cls=MarkupTemplate)
+    globs = kw
+    globs['h'] = pylons_globs['h']
+    globs['c'] = pylons_globs['c']
+    stream = template.generate(**globs)
+    for item in PluginImplementations(IGenshiStreamFilter):
+        stream = item.filter(stream)
+    output = stream.render(method='xhtml', encoding=None, strip_whitespace=True)
+    output = '\n<!-- Snippet %s start -->\n%s\n<!-- Snippet %s end -->\n' % (
+                    template_name, output, template_name)
+    return literal(output)
+
+
+def convert_to_dict(object_type, objs):
+    ''' This is a helper function for converting lists of objects into
+    lists of dicts. It is for backwards compatability only. '''
+
+    def dictize_revision_list(revision, context):
+        # conversionof revision lists
+        def process_names(items):
+            array = []
+            for item in items:
+                array.append(item.name)
+            return array
+
+        rev = {'id' : revision.id,
+               'state' : revision.state,
+               'timestamp' : revision.timestamp,
+               'author' : revision.author,
+               'packages' : process_names(revision.packages),
+               'groups' : process_names(revision.groups),
+               'message' : revision.message,}
+        return rev
+    import lib.dictization.model_dictize as md
+    import ckan.model as model
+    converters = {'package' : md.package_dictize,
+                  'revisions' : dictize_revision_list}
+    converter = converters[object_type]
+    items = []
+    context = {'model' : model}
+    for obj in objs:
+        item = converter(obj, context)
+        items.append(item)
+    return items
diff --git a/ckan/lib/helpers_clean.py b/ckan/lib/helpers_clean.py
index 07ff864..54c50c8 100644
--- a/ckan/lib/helpers_clean.py
+++ b/ckan/lib/helpers_clean.py
@@ -58,6 +58,9 @@
            group_link,
            dump_json,
            auto_log_message,
+           snippet,
+           convert_to_dict,
+           activity_div,
     # imported into ckan.lib.helpers
            literal,
            link_to,
diff --git a/ckan/lib/navl/dictization_functions.py b/ckan/lib/navl/dictization_functions.py
index 8ff547d..d47bd62 100644
--- a/ckan/lib/navl/dictization_functions.py
+++ b/ckan/lib/navl/dictization_functions.py
@@ -1,24 +1,25 @@
 import copy
 import formencode as fe
 import inspect
+from pylons.i18n import _
 
 class Missing(object):
     def __unicode__(self):
-        raise Invalid(fe.api._stdtrans('Missing value'))
+        raise Invalid(_('Missing value'))
     def __str__(self):
-        raise Invalid(fe.api._stdtrans('Missing value'))
+        raise Invalid(_('Missing value'))
     def __int__(self):
-        raise Invalid(fe.api._stdtrans('Missing value'))
+        raise Invalid(_('Missing value'))
     def __complex__(self):
-        raise Invalid(fe.api._stdtrans('Missing value'))
+        raise Invalid(_('Missing value'))
     def __long__(self):
-        raise Invalid(fe.api._stdtrans('Missing value'))
+        raise Invalid(_('Missing value'))
     def __float__(self):
-        raise Invalid(fe.api._stdtrans('Missing value'))
+        raise Invalid(_('Missing value'))
     def __oct__(self):
-        raise Invalid(fe.api._stdtrans('Missing value'))
+        raise Invalid(_('Missing value'))
     def __hex__(self):
-        raise Invalid(fe.api._stdtrans('Missing value'))
+        raise Invalid(_('Missing value'))
     def __nonzero__(self):
         return False
 
diff --git a/ckan/lib/navl/validators.py b/ckan/lib/navl/validators.py
index 3661121..f72e278 100644
--- a/ckan/lib/navl/validators.py
+++ b/ckan/lib/navl/validators.py
@@ -1,6 +1,5 @@
 from dictization_functions import missing, StopOnError, Invalid
-from formencode import validators
-import formencode
+from pylons.i18n import _
 
 def identity_converter(key, data, errors, context):
     return
@@ -15,14 +14,14 @@ def not_missing(key, data, errors, context):
 
     value = data.get(key)
     if value is missing:
-        errors[key].append(formencode.api._stdtrans('Missing value'))
+        errors[key].append(_('Missing value'))
         raise StopOnError
 
 def not_empty(key, data, errors, context):
 
     value = data.get(key)
     if not value or value is missing:
-        errors[key].append(formencode.api._stdtrans('Missing value'))
+        errors[key].append(_('Missing value'))
         raise StopOnError
 
 def if_empty_same_as(other_key):
@@ -42,7 +41,7 @@ def callable(key, data, errors, context):
         other_value = data.get(key[:-1] + (other_key,))
         if (not value or value is missing and
             not other_value or other_value is missing):
-            errors[key].append(formencode.api._stdtrans('Missing value'))
+            errors[key].append(_('Missing value'))
             raise StopOnError
 
     return callable
@@ -52,7 +51,7 @@ def empty(key, data, errors, context):
     value = data.pop(key, None)
     
     if value and value is not missing:
-        errors[key].append(formencode.api._stdtrans(
+        errors[key].append(_(
             'The input field %(name)s was not expected.') % {"name": key[-1]})
 
 def ignore(key, data, errors, context):
@@ -91,5 +90,5 @@ def convert_int(value, context):
     try:
         return int(value)
     except ValueError:
-        raise Invalid(formencode.api._stdtrans('Please enter an integer value'))
+        raise Invalid(_('Please enter an integer value'))
 
diff --git a/ckan/migration/versions/052_update_member_capacities.py b/ckan/migration/versions/052_update_member_capacities.py
new file mode 100644
index 0000000..22a6f33
--- /dev/null
+++ b/ckan/migration/versions/052_update_member_capacities.py
@@ -0,0 +1,12 @@
+from migrate import *
+
+def upgrade(migrate_engine):
+    migrate_engine.execute("""
+    BEGIN;
+        UPDATE member SET capacity='public' WHERE capacity='member'
+                                            AND table_name='package';
+        UPDATE member_revision SET capacity='public' WHERE capacity='member'
+                                                     AND   table_name='package';
+    COMMIT;
+    """
+    )
diff --git a/ckan/model/license.py b/ckan/model/license.py
index f717113..cbed78f 100644
--- a/ckan/model/license.py
+++ b/ckan/model/license.py
@@ -1,4 +1,5 @@
-from pylons import config 
+from pylons import config
+from pylons.i18n import _
 import datetime
 import urllib2
 from ckan.lib.helpers import json
@@ -45,7 +46,210 @@ def __init__(self):
         if group_url:
             self.load_licenses(group_url)
         else:
-            self._create_license_list(self.default_license_list)
+            default_license_list = [
+                {
+                    "domain_content": False, 
+                    "domain_data": False, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "notspecified", 
+                    "is_generic": True, 
+                    "is_okd_compliant": False, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": _("License Not Specified"), 
+                    "url": ""
+                }, 
+                {
+                    "domain_content": False, 
+                    "domain_data": True, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "odc-pddl", 
+                    "is_okd_compliant": True, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": "Open Data Commons Public Domain Dedication and Licence (PDDL)", 
+                    "url": "http://www.opendefinition.org/licenses/odc-pddl"
+                }, 
+                {
+                    "domain_content": False, 
+                    "domain_data": True, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "odc-odbl", 
+                    "is_okd_compliant": True, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": "Open Data Commons Open Database License (ODbL)", 
+                    "url": "http://www.opendefinition.org/licenses/odc-odbl"
+                }, 
+                {
+                    "domain_content": False, 
+                    "domain_data": True, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "odc-by", 
+                    "is_okd_compliant": True, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": "Open Data Commons Attribution License", 
+                    "url": "http://www.opendefinition.org/licenses/odc-by"
+                }, 
+                {
+                    "domain_content": True, 
+                    "domain_data": True, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "cc-zero", 
+                    "is_okd_compliant": True, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": "Creative Commons CCZero", 
+                    "url": "http://www.opendefinition.org/licenses/cc-zero"
+                }, 
+                {
+                    "domain_content": True, 
+                    "domain_data": False, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "cc-by", 
+                    "is_okd_compliant": True, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": "Creative Commons Attribution", 
+                    "url": "http://www.opendefinition.org/licenses/cc-by"
+                }, 
+                {
+                    "domain_content": True, 
+                    "domain_data": False, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "cc-by-sa", 
+                    "is_okd_compliant": True, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": "Creative Commons Attribution Share-Alike", 
+                    "url": "http://www.opendefinition.org/licenses/cc-by-sa"
+                }, 
+                {
+                    "domain_content": True, 
+                    "domain_data": False, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "gfdl", 
+                    "is_okd_compliant": True, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": "GNU Free Documentation License", 
+                    "url": "http://www.opendefinition.org/licenses/gfdl"
+                }, 
+                {
+                    "domain_content": True, 
+                    "domain_data": False, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "other-open", 
+                    "is_generic": True, 
+                    "is_okd_compliant": True, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": _("Other (Open)"), 
+                    "url": ""
+                }, 
+                {
+                    "domain_content": True, 
+                    "domain_data": False, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "other-pd", 
+                    "is_generic": True, 
+                    "is_okd_compliant": True, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": _("Other (Public Domain)"), 
+                    "url": ""
+                }, 
+                {
+                    "domain_content": True, 
+                    "domain_data": False, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "other-at", 
+                    "is_generic": True, 
+                    "is_okd_compliant": True, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": _("Other (Attribution)"), 
+                    "url": ""
+                }, 
+                {
+                    "domain_content": True, 
+                    "domain_data": False, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "uk-ogl", 
+                    "is_okd_compliant": True, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": "UK Open Government Licence (OGL)", 
+                    "url": "http://reference.data.gov.uk/id/open-government-licence"
+                }, 
+                {
+                    "domain_content": False, 
+                    "domain_data": False, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "cc-nc", 
+                    "is_okd_compliant": False, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": "Creative Commons Non-Commercial (Any)", 
+                    "url": "http://creativecommons.org/licenses/by-nc/2.0/"
+                }, 
+                {
+                    "domain_content": False, 
+                    "domain_data": False, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "other-nc", 
+                    "is_generic": True, 
+                    "is_okd_compliant": False, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": _("Other (Non-Commercial)"), 
+                    "url": ""
+                }, 
+                {
+                    "domain_content": False, 
+                    "domain_data": False, 
+                    "domain_software": False, 
+                    "family": "", 
+                    "id": "other-closed", 
+                    "is_generic": True, 
+                    "is_okd_compliant": False, 
+                    "is_osi_compliant": False, 
+                    "maintainer": "", 
+                    "status": "active", 
+                    "title": _("Other (Not Open)"), 
+                    "url": ""
+                }
+                ]
+            self._create_license_list(default_license_list)
 
     def load_licenses(self, license_url):
         try:
@@ -96,207 +300,3 @@ def __iter__(self):
 
     def __len__(self):
         return len(self.licenses)
-
-    default_license_list = [
-          {
-            "domain_content": False, 
-            "domain_data": False, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "notspecified", 
-            "is_generic": True, 
-            "is_okd_compliant": False, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "License Not Specified", 
-            "url": ""
-          }, 
-          {
-            "domain_content": False, 
-            "domain_data": True, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "odc-pddl", 
-            "is_okd_compliant": True, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "Open Data Commons Public Domain Dedication and Licence (PDDL)", 
-            "url": "http://www.opendefinition.org/licenses/odc-pddl"
-          }, 
-          {
-            "domain_content": False, 
-            "domain_data": True, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "odc-odbl", 
-            "is_okd_compliant": True, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "Open Data Commons Open Database License (ODbL)", 
-            "url": "http://www.opendefinition.org/licenses/odc-odbl"
-          }, 
-          {
-            "domain_content": False, 
-            "domain_data": True, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "odc-by", 
-            "is_okd_compliant": True, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "Open Data Commons Attribution License", 
-            "url": "http://www.opendefinition.org/licenses/odc-by"
-          }, 
-          {
-            "domain_content": True, 
-            "domain_data": True, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "cc-zero", 
-            "is_okd_compliant": True, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "Creative Commons CCZero", 
-            "url": "http://www.opendefinition.org/licenses/cc-zero"
-          }, 
-          {
-            "domain_content": True, 
-            "domain_data": False, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "cc-by", 
-            "is_okd_compliant": True, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "Creative Commons Attribution", 
-            "url": "http://www.opendefinition.org/licenses/cc-by"
-          }, 
-          {
-            "domain_content": True, 
-            "domain_data": False, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "cc-by-sa", 
-            "is_okd_compliant": True, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "Creative Commons Attribution Share-Alike", 
-            "url": "http://www.opendefinition.org/licenses/cc-by-sa"
-          }, 
-          {
-            "domain_content": True, 
-            "domain_data": False, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "gfdl", 
-            "is_okd_compliant": True, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "GNU Free Documentation License", 
-            "url": "http://www.opendefinition.org/licenses/gfdl"
-          }, 
-          {
-            "domain_content": True, 
-            "domain_data": False, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "other-open", 
-            "is_generic": True, 
-            "is_okd_compliant": True, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "Other (Open)", 
-            "url": ""
-          }, 
-          {
-            "domain_content": True, 
-            "domain_data": False, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "other-pd", 
-            "is_generic": True, 
-            "is_okd_compliant": True, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "Other (Public Domain)", 
-            "url": ""
-          }, 
-          {
-            "domain_content": True, 
-            "domain_data": False, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "other-at", 
-            "is_generic": True, 
-            "is_okd_compliant": True, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "Other (Attribution)", 
-            "url": ""
-          }, 
-          {
-            "domain_content": True, 
-            "domain_data": False, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "uk-ogl", 
-            "is_okd_compliant": True, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "UK Open Government Licence (OGL)", 
-            "url": "http://reference.data.gov.uk/id/open-government-licence"
-          }, 
-          {
-            "domain_content": False, 
-            "domain_data": False, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "cc-nc", 
-            "is_okd_compliant": False, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "Creative Commons Non-Commercial (Any)", 
-            "url": "http://creativecommons.org/licenses/by-nc/2.0/"
-          }, 
-          {
-            "domain_content": False, 
-            "domain_data": False, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "other-nc", 
-            "is_generic": True, 
-            "is_okd_compliant": False, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "Other (Non-Commercial)", 
-            "url": ""
-          }, 
-          {
-            "domain_content": False, 
-            "domain_data": False, 
-            "domain_software": False, 
-            "family": "", 
-            "id": "other-closed", 
-            "is_generic": True, 
-            "is_okd_compliant": False, 
-            "is_osi_compliant": False, 
-            "maintainer": "", 
-            "status": "active", 
-            "title": "Other (Not Open)", 
-            "url": ""
-          }
-        ]
diff --git a/ckan/public/scripts/application.js b/ckan/public/scripts/application.js
index 2da9806..40513fd 100644
--- a/ckan/public/scripts/application.js
+++ b/ckan/public/scripts/application.js
@@ -113,7 +113,7 @@ CKAN.Utils = CKAN.Utils || {};
 	}
 	if ($('#login').length){
 		$('#login').submit( function () {
-			$.ajax('CKAN.SITE_URL + /user/set_lang/' + CKAN.LANG, {async:false});
+			$.ajax(CKAN.SITE_URL + '/user/set_lang/' + CKAN.LANG, {async:false});
 		});
 	}
   });
@@ -434,14 +434,14 @@ CKAN.View.Resource = Backbone.View.extend({
         num: this.options.position,
         resource_icon: '/images/icons/page_white.png',
         resourceTypeOptions: [
-          ['file', 'Data File'],
-          ['api', 'API'],
-          ['visualization', 'Visualization'],
-          ['image', 'Image'],
-          ['metadata', 'Metadata'],
-          ['documentation', 'Documentation'],
-          ['code', 'Code'],
-          ['example', 'Example']
+          ['file', CKAN.Strings.dataFile],
+          ['api', CKAN.Strings.api],
+          ['visualization', CKAN.Strings.visualization],
+          ['image', CKAN.Strings.image],
+          ['metadata', CKAN.Strings.metadata],
+          ['documentation', CKAN.Strings.documentation],
+          ['code', CKAN.Strings.code],
+          ['example', CKAN.Strings.example]
         ]
     };
     // Generate DOM elements
@@ -1277,6 +1277,13 @@ CKAN.DataPreview = function ($, my) {
           view: new recline.View.FlotGraph({
             model: dataset
           })
+        },
+        {
+          id: 'map',
+          label: 'Map',
+          view: new recline.View.Map({
+            model: dataset
+          })
         }
       ];
       var dataExplorer = new recline.View.DataExplorer({
diff --git a/ckan/public/scripts/templates.js b/ckan/public/scripts/templates.js
index 3cd8251..66b06cb 100644
--- a/ckan/public/scripts/templates.js
+++ b/ckan/public/scripts/templates.js
@@ -26,6 +26,10 @@ CKAN.Templates.resourceEntry = ' \
     </a>\
   </li>';
 
+var youCanUseMarkdownString = CKAN.Strings.youCanUseMarkdown.replace('%a', '<a href="http://daringfireball.net/projects/markdown/syntax" target="_blank">').replace('%b', '</a>');
+var shouldADataStoreBeEnabledString = CKAN.Strings.shouldADataStoreBeEnabled.replace('%a', '<a href="http://docs.ckan.org/en/latest/datastore.html" target="_blank">').replace('%b', '</a>');
+var datesAreInISOString = CKAN.Strings.datesAreInISO.replace('%a', '<a href="http://en.wikipedia.org/wiki/ISO_8601#Calendar_dates" target="_blank">').replace('%b', '</a>').replace('%c', '<strong>').replace('%d', '</strong>');
+
 // TODO it would be nice to unify this with the markdown editor specified in helpers.py
 CKAN.Templates.resourceDetails = ' \
   <div style="display: none;" class="resource-details"> \
@@ -50,7 +54,7 @@ CKAN.Templates.resourceDetails = ' \
             <textarea class="js-resource-edit-description markdown-input" name="resources__${num}__description">${resource.description}</textarea> \
           </div> \
           <div class="markdown-preview" style="display: none;"></div> \
-          <span class="hints">You can use <a href="http://daringfireball.net/projects/markdown/syntax" target="_blank">Markdown formatting</a> here.</span> \
+          <span class="hints">'+youCanUseMarkdownString+'</span> \
         </div> \
       </div> \
     </div> \
@@ -77,7 +81,7 @@ CKAN.Templates.resourceDetails = ' \
       <label for="" class="control-label" property="rdfs:label">'+CKAN.Strings.resourceType+'</label> \
       <div class="controls"> \
         {{if resource.resource_type=="file.upload"}} \
-          Data File (Uploaded) \
+          '+CKAN.Strings.dataFileUploaded+' \
           <input name="resources__${num}__resource_type" type="hidden" value="${resource.resource_type}" /> \
         {{/if}} \
         {{if resource.resource_type!="file.upload"}} \
@@ -95,7 +99,7 @@ CKAN.Templates.resourceDetails = ' \
         <label class="checkbox"> \
           <input type="checkbox" class="js-datastore-enabled-checkbox" /> \
           <input type="hidden" name="resources__${num}__webstore_url" value="${resource.webstore_url}" class="js-datastore-enabled-text" /> \
-          <span class="hint">Should a <a href="http://docs.ckan.org/en/latest/datastore.html" target="_blank">DataStore table and Data API</a> be enabled for this resource?</span> \
+          <span class="hint">'+shouldADataStoreBeEnabledString+'</span> \
         </label> \
       </div> \
     </div> \
@@ -103,7 +107,7 @@ CKAN.Templates.resourceDetails = ' \
       <label for="" class="control-label" property="rdfs:label">'+CKAN.Strings.lastModified+'</label> \
       <div class="controls"> \
         <input class="input-small" name="resources__${num}__last_modified" type="text" value="${resource.last_modified}" /> \
-        <div class="hint">Dates are in <a href="http://en.wikipedia.org/wiki/ISO_8601#Calendar_dates" target="_blank">ISO Format</a> — eg. <strong>2012-12-25</strong> or <strong>2010-05-31T14:30</strong>.</div> \
+        <div class="hint">'+datesAreInISOString+'</div> \
       </div> \
     </div> \
     <div class="control-group"> \
diff --git a/ckan/public/scripts/vendor/leaflet/0.3.1/images/layers.png b/ckan/public/scripts/vendor/leaflet/0.3.1/images/layers.png
new file mode 100644
index 0000000..9be965f
Binary files /dev/null and b/ckan/public/scripts/vendor/leaflet/0.3.1/images/layers.png differ
diff --git a/ckan/public/scripts/vendor/leaflet/0.3.1/images/marker-shadow.png b/ckan/public/scripts/vendor/leaflet/0.3.1/images/marker-shadow.png
new file mode 100644
index 0000000..a64f6a6
Binary files /dev/null and b/ckan/public/scripts/vendor/leaflet/0.3.1/images/marker-shadow.png differ
diff --git a/ckan/public/scripts/vendor/leaflet/0.3.1/images/marker.png b/ckan/public/scripts/vendor/leaflet/0.3.1/images/marker.png
new file mode 100644
index 0000000..bef032e
Binary files /dev/null and b/ckan/public/scripts/vendor/leaflet/0.3.1/images/marker.png differ
diff --git a/ckan/public/scripts/vendor/leaflet/0.3.1/images/popup-close.png b/ckan/public/scripts/vendor/leaflet/0.3.1/images/popup-close.png
new file mode 100644
index 0000000..c8faec5
Binary files /dev/null and b/ckan/public/scripts/vendor/leaflet/0.3.1/images/popup-close.png differ
diff --git a/ckan/public/scripts/vendor/leaflet/0.3.1/images/zoom-in.png b/ckan/public/scripts/vendor/leaflet/0.3.1/images/zoom-in.png
new file mode 100644
index 0000000..9f473d6
Binary files /dev/null and b/ckan/public/scripts/vendor/leaflet/0.3.1/images/zoom-in.png differ
diff --git a/ckan/public/scripts/vendor/leaflet/0.3.1/images/zoom-out.png b/ckan/public/scripts/vendor/leaflet/0.3.1/images/zoom-out.png
new file mode 100644
index 0000000..f0a5b5d
Binary files /dev/null and b/ckan/public/scripts/vendor/leaflet/0.3.1/images/zoom-out.png differ
diff --git a/ckan/public/scripts/vendor/leaflet/0.3.1/leaflet.css b/ckan/public/scripts/vendor/leaflet/0.3.1/leaflet.css
new file mode 100644
index 0000000..119ddb2
--- /dev/null
+++ b/ckan/public/scripts/vendor/leaflet/0.3.1/leaflet.css
@@ -0,0 +1,323 @@
+/* required styles */
+
+.leaflet-map-pane,
+.leaflet-tile,
+.leaflet-marker-icon,
+.leaflet-marker-shadow,
+.leaflet-tile-pane,
+.leaflet-overlay-pane,
+.leaflet-shadow-pane,
+.leaflet-marker-pane,
+.leaflet-popup-pane,
+.leaflet-overlay-pane svg,
+.leaflet-zoom-box,
+.leaflet-image-layer { /* TODO optimize classes */
+	position: absolute;
+	}
+.leaflet-container {
+	overflow: hidden;
+	}
+.leaflet-tile-pane, .leaflet-container {
+	-webkit-transform: translate3d(0,0,0);
+	}
+.leaflet-tile,
+.leaflet-marker-icon,
+.leaflet-marker-shadow {
+	-moz-user-select: none;
+	-webkit-user-select: none;
+	user-select: none;
+	}
+.leaflet-marker-icon,
+.leaflet-marker-shadow {
+	display: block;
+	}
+.leaflet-clickable {
+	cursor: pointer;
+	}
+.leaflet-container img {
+	max-width: none !important;
+	}
+
+.leaflet-tile-pane { z-index: 2; }
+
+.leaflet-objects-pane { z-index: 3; }
+.leaflet-overlay-pane { z-index: 4; }
+.leaflet-shadow-pane { z-index: 5; }
+.leaflet-marker-pane { z-index: 6; }
+.leaflet-popup-pane { z-index: 7; }
+
+.leaflet-zoom-box {
+	width: 0;
+	height: 0;
+	}
+
+.leaflet-tile {
+	visibility: hidden;
+	}
+.leaflet-tile-loaded {
+	visibility: inherit;
+	}
+
+a.leaflet-active {
+	outline: 2px solid orange;
+	}
+
+
+/* Leaflet controls */
+
+.leaflet-control {
+	position: relative;
+	z-index: 7;
+	}
+.leaflet-top,
+.leaflet-bottom {
+	position: absolute;
+	}
+.leaflet-top {
+	top: 0;
+	}
+.leaflet-right {
+	right: 0;
+	}
+.leaflet-bottom {
+	bottom: 0;
+	}
+.leaflet-left {
+	left: 0;
+	}
+.leaflet-control {
+	float: left;
+	clear: both;
+	}
+.leaflet-right .leaflet-control {
+	float: right;
+	}
+.leaflet-top .leaflet-control {
+	margin-top: 10px;
+	}
+.leaflet-bottom .leaflet-control {
+	margin-bottom: 10px;
+	}
+.leaflet-left .leaflet-control {
+	margin-left: 10px;
+	}
+.leaflet-right .leaflet-control {
+	margin-right: 10px;
+	}
+
+.leaflet-control-zoom, .leaflet-control-layers {
+	-moz-border-radius: 7px;
+	-webkit-border-radius: 7px;
+	border-radius: 7px;
+	}
+.leaflet-control-zoom {
+	padding: 5px;
+	background: rgba(0, 0, 0, 0.25);
+	}
+.leaflet-control-zoom a {
+	background-color: rgba(255, 255, 255, 0.75);
+	}
+.leaflet-control-zoom a, .leaflet-control-layers a {
+	background-position: 50% 50%;
+	background-repeat: no-repeat;
+	display: block;
+	}
+.leaflet-control-zoom a {
+	-moz-border-radius: 4px;
+	-webkit-border-radius: 4px;
+	border-radius: 4px;
+	width: 19px;
+	height: 19px;
+	}
+.leaflet-control-zoom a:hover {
+	background-color: #fff;
+	}
+.leaflet-big-buttons .leaflet-control-zoom a {
+	width: 27px;
+	height: 27px;
+	}
+.leaflet-control-zoom-in {
+	background-image: url(images/zoom-in.png);
+	margin-bottom: 5px;
+	}
+.leaflet-control-zoom-out {
+	background-image: url(images/zoom-out.png);
+	}
+
+.leaflet-control-layers {
+	-moz-box-shadow: 0 0 7px #999;
+	-webkit-box-shadow: 0 0 7px #999;
+	box-shadow: 0 0 7px #999;
+
+	background: #f8f8f9;
+	}
+.leaflet-control-layers a {
+	background-image: url(images/layers.png);
+	width: 36px;
+	height: 36px;
+	}
+.leaflet-big-buttons .leaflet-control-layers a {
+	width: 44px;
+	height: 44px;
+	}
+.leaflet-control-layers .leaflet-control-layers-list,
+.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
+	display: none;
+	}
+.leaflet-control-layers-expanded .leaflet-control-layers-list {
+	display: block;
+	position: relative;
+	}
+.leaflet-control-layers-expanded {
+	padding: 6px 10px 6px 6px;
+	font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
+	color: #333;
+	background: #fff;
+	}
+.leaflet-control-layers input {
+	margin-top: 2px;
+	position: relative;
+	top: 1px;
+	}
+.leaflet-control-layers label {
+	display: block;
+	}
+.leaflet-control-layers-separator {
+	height: 0;
+	border-top: 1px solid #ddd;
+	margin: 5px -10px 5px -6px;
+	}
+
+.leaflet-container .leaflet-control-attribution {
+	margin: 0;
+	padding: 0 5px;
+
+	font: 11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
+	color: #333;
+
+	background-color: rgba(255, 255, 255, 0.7);
+
+	-moz-box-shadow: 0 0 7px #ccc;
+	-webkit-box-shadow: 0 0 7px #ccc;
+	box-shadow: 0 0 7px #ccc;
+	}
+
+
+/* Fade animations */
+
+.leaflet-fade-anim .leaflet-tile {
+	opacity: 0;
+
+	-webkit-transition: opacity 0.2s linear;
+	-moz-transition: opacity 0.2s linear;
+	-o-transition: opacity 0.2s linear;
+	transition: opacity 0.2s linear;
+	}
+.leaflet-fade-anim .leaflet-tile-loaded {
+	opacity: 1;
+	}
+
+.leaflet-fade-anim .leaflet-popup {
+	opacity: 0;
+
+	-webkit-transition: opacity 0.2s linear;
+	-moz-transition: opacity 0.2s linear;
+	-o-transition: opacity 0.2s linear;
+	transition: opacity 0.2s linear;
+	}
+.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
+	opacity: 1;
+	}
+
+.leaflet-zoom-anim .leaflet-tile {
+	-webkit-transition: none;
+	-moz-transition: none;
+	-o-transition: none;
+	transition: none;
+	}
+
+.leaflet-zoom-anim .leaflet-objects-pane {
+	visibility: hidden;
+	}
+
+
+/* Popup layout */
+
+.leaflet-popup {
+	position: absolute;
+	text-align: center;
+	-webkit-transform: translate3d(0,0,0);
+	}
+.leaflet-popup-content-wrapper {
+	padding: 1px;
+	text-align: left;
+	}
+.leaflet-popup-content {
+	margin: 19px;
+	}
+.leaflet-popup-tip-container {
+	margin: 0 auto;
+	width: 40px;
+	height: 16px;
+	position: relative;
+	overflow: hidden;
+	}
+.leaflet-popup-tip {
+	width: 15px;
+	height: 15px;
+	padding: 1px;
+
+	margin: -8px auto 0;
+
+	-moz-transform: rotate(45deg);
+	-webkit-transform: rotate(45deg);
+	-ms-transform: rotate(45deg);
+	-o-transform: rotate(45deg);
+	transform: rotate(45deg);
+	}
+.leaflet-popup-close-button {
+	position: absolute;
+	top: 9px;
+	right: 9px;
+
+	width: 10px;
+	height: 10px;
+
+	overflow: hidden;
+	}
+.leaflet-popup-content p {
+	margin: 18px 0;
+	}
+
+
+/* Visual appearance */
+
+.leaflet-container {
+	background: #ddd;
+	}
+.leaflet-container a {
+	color: #0078A8;
+	}
+.leaflet-zoom-box {
+	border: 2px dotted #05f;
+	background: white;
+	opacity: 0.5;
+	}
+.leaflet-popup-content-wrapper, .leaflet-popup-tip {
+	background: white;
+
+	box-shadow: 0 1px 10px #888;
+	-moz-box-shadow: 0 1px 10px #888;
+	 -webkit-box-shadow: 0 1px 14px #999;
+	}
+.leaflet-popup-content-wrapper {
+	-moz-border-radius: 20px;
+	-webkit-border-radius: 20px;
+	border-radius: 20px;
+	}
+.leaflet-popup-content {
+	font: 12px/1.4 "Helvetica Neue", Arial, Helvetica, sans-serif;
+	}
+.leaflet-popup-close-button {
+	background: white url(images/popup-close.png);
+	}
diff --git a/ckan/public/scripts/vendor/leaflet/0.3.1/leaflet.ie.css b/ckan/public/scripts/vendor/leaflet/0.3.1/leaflet.ie.css
new file mode 100644
index 0000000..a120c0c
--- /dev/null
+++ b/ckan/public/scripts/vendor/leaflet/0.3.1/leaflet.ie.css
@@ -0,0 +1,48 @@
+.leaflet-tile {
+	filter: inherit;
+	}
+
+.leaflet-vml-shape {
+	width: 1px;
+	height: 1px;
+	}
+.lvml {
+	behavior: url(#default#VML); 
+	display: inline-block; 
+	position: absolute;
+	}
+	
+.leaflet-control {
+	display: inline;
+	}
+
+.leaflet-popup-tip {
+	width: 21px;
+	_width: 27px;
+	margin: 0 auto;
+	_margin-top: -3px;
+	
+	filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
+	-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
+	}
+.leaflet-popup-tip-container {
+	margin-top: -1px;
+	}
+.leaflet-popup-content-wrapper, .leaflet-popup-tip {
+	border: 1px solid #bbb;
+	}
+
+.leaflet-control-zoom {
+	filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#3F000000',EndColorStr='#3F000000');
+	}
+.leaflet-control-zoom a {
+	background-color: #eee;
+	}
+.leaflet-control-zoom a:hover {
+	background-color: #fff;
+	}
+.leaflet-control-layers-toggle {
+	}
+.leaflet-control-attribution, .leaflet-control-layers {
+	background: white;
+	}
\ No newline at end of file
diff --git a/ckan/public/scripts/vendor/leaflet/0.3.1/leaflet.js b/ckan/public/scripts/vendor/leaflet/0.3.1/leaflet.js
new file mode 100644
index 0000000..a2bebba
--- /dev/null
+++ b/ckan/public/scripts/vendor/leaflet/0.3.1/leaflet.js
@@ -0,0 +1,6 @@
+/*
+ Copyright (c) 2010-2011, CloudMade, Vladimir Agafonkin
+ Leaflet is a modern open-source JavaScript library for interactive maps.
+ http://leaflet.cloudmade.com
+*/
+(function(a){a.L={VERSION:"0.3",ROOT_URL:a.L_ROOT_URL||function(){var a=document.getElementsByTagName("script"),b=/\/?leaflet[\-\._]?([\w\-\._]*)\.js\??/,c,d,e,f;for(c=0,d=a.length;c<d;c++){e=a[c].src,f=e.match(b);if(f)return f[1]==="include"?"../../dist/":e.split(b)[0]+"/"}return""}(),noConflict:function(){return a.L=this._originalL,this},_originalL:a.L}})(this),L.Util={extend:function(a){var b=Array.prototype.slice.call(arguments,1);for(var c=0,d=b.length,e;c<d;c++){e=b[c]||{};for(var f in e)e.hasOwnProperty(f)&&(a[f]=e[f])}return a},bind:function(a,b){return function(){return a.apply(b,arguments)}},stamp:function(){var a=0,b="_leaflet_id";return function(c){return c[b]=c[b]||++a,c[b]}}(),requestAnimFrame:function(){function a(a){window.setTimeout(a,1e3/60)}var b=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||a;return function(c,d,e,f){c=d?L.Util.bind(c,d):c,e
 &&b===a?c():b(c,f)}}(),limitExecByInterval:function(a,b,c){function g(){d=!1,e&&(f.callee.apply(c,f),e=!1)}var d,e,f;return function(){f=arguments,d?e=!0:(d=!0,setTimeout(g,b),a.apply(c,f))}},falseFn:function(){return!1},formatNum:function(a,b){var c=Math.pow(10,b||5);return Math.round(a*c)/c},setOptions:function(a,b){a.options=L.Util.extend({},a.options,b)},getParamString:function(a){var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c+"="+a[c]);return"?"+b.join("&")},template:function(a,b){return a.replace(/\{ *([\w_]+) *\}/g,function(a,c){var d=b[c];if(!b.hasOwnProperty(c))throw Error("No value provided for variable "+a);return d})}},L.Class=function(){},L.Class.extend=function(a){var b=function(){this.initialize&&this.initialize.apply(this,arguments)},c=function(){};c.prototype=this.prototype;var d=new c;d.constructor=b,b.prototype=d,b.superclass=this.prototype;for(var e in this)this.hasOwnProperty(e)&&e!=="prototype"&&e!=="superclass"&&(b[e]=this[e]);return a.statics&&
 (L.Util.extend(b,a.statics),delete a.statics),a.includes&&(L.Util.extend.apply(null,[d].concat(a.includes)),delete a.includes),a.options&&d.options&&(a.options=L.Util.extend({},d.options,a.options)),L.Util.extend(d,a),b.extend=L.Class.extend,b.include=function(a){L.Util.extend(this.prototype,a)},b},L.Mixin={},L.Mixin.Events={addEventListener:function(a,b,c){var d=this._leaflet_events=this._leaflet_events||{};return d[a]=d[a]||[],d[a].push({action:b,context:c||this}),this},hasEventListeners:function(a){var b="_leaflet_events";return b in this&&a in this[b]&&this[b][a].length>0},removeEventListener:function(a,b,c){if(!this.hasEventListeners(a))return this;for(var d=0,e=this._leaflet_events,f=e[a].length;d<f;d++)if(e[a][d].action===b&&(!c||e[a][d].context===c))return e[a].splice(d,1),this;return this},fireEvent:function(a,b){if(!this.hasEventListeners(a))return this;var c=L.Util.extend({type:a,target:this},b),d=this._leaflet_events[a].slice();for(var e=0,f=d.length;e<f;e++)d[e]
 .action.call(d[e].context||this,c);return this}},L.Mixin.Events.on=L.Mixin.Events.addEventListener,L.Mixin.Events.off=L.Mixin.Events.removeEventListener,L.Mixin.Events.fire=L.Mixin.Events.fireEvent,function(){var a=navigator.userAgent.toLowerCase(),b=!!window.ActiveXObject,c=a.indexOf("webkit")!==-1,d=typeof orientation!="undefined"?!0:!1,e=a.indexOf("android")!==-1,f=window.opera;L.Browser={ie:b,ie6:b&&!window.XMLHttpRequest,webkit:c,webkit3d:c&&"WebKitCSSMatrix"in window&&"m11"in new window.WebKitCSSMatrix,gecko:a.indexOf("gecko")!==-1,opera:f,android:e,mobileWebkit:d&&c,mobileOpera:d&&f,mobile:d,touch:function(){var a=!1,b="ontouchstart";if(b in document.documentElement)return!0;var c=document.createElement("div");return!c.setAttribute||!c.removeAttribute?!1:(c.setAttribute(b,"return;"),typeof c[b]=="function"&&(a=!0),c.removeAttribute(b),c=null,a)}()}}(),L.Point=function(a,b,c){this.x=c?Math.round(a):a,this.y=c?Math.round(b):b},L.Point.prototype={add:function(a){return t
 his.clone()._add(a)},_add:function(a){return this.x+=a.x,this.y+=a.y,this},subtract:function(a){return this.clone()._subtract(a)},_subtract:function(a){return this.x-=a.x,this.y-=a.y,this},divideBy:function(a,b){return new L.Point(this.x/a,this.y/a,b)},multiplyBy:function(a){return new L.Point(this.x*a,this.y*a)},distanceTo:function(a){var b=a.x-this.x,c=a.y-this.y;return Math.sqrt(b*b+c*c)},round:function(){return this.clone()._round()},_round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},clone:function(){return new L.Point(this.x,this.y)},toString:function(){return"Point("+L.Util.formatNum(this.x)+", "+L.Util.formatNum(this.y)+")"}},L.Bounds=L.Class.extend({initialize:function(a,b){if(!a)return;var c=a instanceof Array?a:[a,b];for(var d=0,e=c.length;d<e;d++)this.extend(c[d])},extend:function(a){!this.min&&!this.max?(this.min=new L.Point(a.x,a.y),this.max=new L.Point(a.x,a.y)):(this.min.x=Math.min(a.x,this.min.x),this.max.x=Math.max(a.x,this.ma
 x.x),this.min.y=Math.min(a.y,this.min.y),this.max.y=Math.max(a.y,this.max.y))},getCenter:function(a){return new L.Point((this.min.x+this.max.x)/2,(this.min.y+this.max.y)/2,a)},contains:function(a){var b,c;return a instanceof L.Bounds?(b=a.min,c=a.max):b=c=a,b.x>=this.min.x&&c.x<=this.max.x&&b.y>=this.min.y&&c.y<=this.max.y},intersects:function(a){var b=this.min,c=this.max,d=a.min,e=a.max,f=e.x>=b.x&&d.x<=c.x,g=e.y>=b.y&&d.y<=c.y;return f&&g}}),L.Transformation=L.Class.extend({initialize:function(a,b,c,d){this._a=a,this._b=b,this._c=c,this._d=d},transform:function(a,b){return this._transform(a.clone(),b)},_transform:function(a,b){return b=b||1,a.x=b*(this._a*a.x+this._b),a.y=b*(this._c*a.y+this._d),a},untransform:function(a,b){return b=b||1,new L.Point((a.x/b-this._b)/this._a,(a.y/b-this._d)/this._c)}}),L.DomUtil={get:function(a){return typeof a=="string"?document.getElementById(a):a},getStyle:function(a,b){var c=a.style[b];!c&&a.currentStyle&&(c=a.currentStyle[b]);if(!c||c==
 ="auto"){var d=document.defaultView.getComputedStyle(a,null);c=d?d[b]:null}return c==="auto"?null:c},getViewportOffset:function(a){var b=0,c=0,d=a,e=document.body;do{b+=d.offsetTop||0,c+=d.offsetLeft||0;if(d.offsetParent===e&&L.DomUtil.getStyle(d,"position")==="absolute")break;d=d.offsetParent}while(d);d=a;do{if(d===e)break;b-=d.scrollTop||0,c-=d.scrollLeft||0,d=d.parentNode}while(d);return new L.Point(c,b)},create:function(a,b,c){var d=document.createElement(a);return d.className=b,c&&c.appendChild(d),d},disableTextSelection:function(){document.selection&&document.selection.empty&&document.selection.empty(),this._onselectstart||(this._onselectstart=document.onselectstart,document.onselectstart=L.Util.falseFn)},enableTextSelection:function(){document.onselectstart=this._onselectstart,this._onselectstart=null},hasClass:function(a,b){return a.className.length>0&&RegExp("(^|\\s)"+b+"(\\s|$)").test(a.className)},addClass:function(a,b){L.DomUtil.hasClass(a,b)||(a.className+=(a.cl
 assName?" ":"")+b)},removeClass:function(a,b){a.className=a.className.replace(/(\S+)\s*/g,function(a,c){return c===b?"":a}).replace(/^\s+/,"")},setOpacity:function(a,b){L.Browser.ie?a.style.filter="alpha(opacity="+Math.round(b*100)+")":a.style.opacity=b},testProp:function(a){var b=document.documentElement.style;for(var c=0;c<a.length;c++)if(a[c]in b)return a[c];return!1},getTranslateString:function(a){return L.DomUtil.TRANSLATE_OPEN+a.x+"px,"+a.y+"px"+L.DomUtil.TRANSLATE_CLOSE},getScaleString:function(a,b){var c=L.DomUtil.getTranslateString(b),d=" scale("+a+") ",e=L.DomUtil.getTranslateString(b.multiplyBy(-1));return c+d+e},setPosition:function(a,b){a._leaflet_pos=b,L.Browser.webkit3d?(a.style[L.DomUtil.TRANSFORM]=L.DomUtil.getTranslateString(b),L.Browser.android&&(a.style["-webkit-perspective"]="1000",a.style["-webkit-backface-visibility"]="hidden")):(a.style.left=b.x+"px",a.style.top=b.y+"px")},getPosition:function(a){return a._leaflet_pos}},L.Util.extend(L.DomUtil,{TRANSI
 TION:L.DomUtil.testProp(["transition","webkitTransition","OTransition","MozTransition","msTransition"]),TRANSFORM:L.DomUtil.testProp(["transformProperty","WebkitTransform","OTransform","MozTransform","msTransform"]),TRANSLATE_OPEN:"translate"+(L.Browser.webkit3d?"3d(":"("),TRANSLATE_CLOSE:L.Browser.webkit3d?",0)":")"}),L.LatLng=function(a,b,c){var d=parseFloat(a),e=parseFloat(b);if(isNaN(d)||isNaN(e))throw Error("Invalid LatLng object: ("+a+", "+b+")");c!==!0&&(d=Math.max(Math.min(d,90),-90),e=(e+180)%360+(e<-180||e===180?180:-180)),this.lat=d,this.lng=e},L.Util.extend(L.LatLng,{DEG_TO_RAD:Math.PI/180,RAD_TO_DEG:180/Math.PI,MAX_MARGIN:1e-9}),L.LatLng.prototype={equals:function(a){if(a instanceof L.LatLng){var b=Math.max(Math.abs(this.lat-a.lat),Math.abs(this.lng-a.lng));return b<=L.LatLng.MAX_MARGIN}return!1},toString:function(){return"LatLng("+L.Util.formatNum(this.lat)+", "+L.Util.formatNum(this.lng)+")"},distanceTo:function(a){var b=6378137,c=L.LatLng.DEG_TO_RAD,d=(a.lat-
 this.lat)*c,e=(a.lng-this.lng)*c,f=this.lat*c,g=a.lat*c,h=Math.sin(d/2),i=Math.sin(e/2),j=h*h+i*i*Math.cos(f)*Math.cos(g);return b*2*Math.atan2(Math.sqrt(j),Math.sqrt(1-j))}},L.LatLngBounds=L.Class.extend({initialize:function(a,b){if(!a)return;var c=a instanceof Array?a:[a,b];for(var d=0,e=c.length;d<e;d++)this.extend(c[d])},extend:function(a){!this._southWest&&!this._northEast?(this._southWest=new L.LatLng(a.lat,a.lng,!0),this._northEast=new L.LatLng(a.lat,a.lng,!0)):(this._southWest.lat=Math.min(a.lat,this._southWest.lat),this._southWest.lng=Math.min(a.lng,this._southWest.lng),this._northEast.lat=Math.max(a.lat,this._northEast.lat),this._northEast.lng=Math.max(a.lng,this._northEast.lng))},getCenter:function(){return new L.LatLng((this._southWest.lat+this._northEast.lat)/2,(this._southWest.lng+this._northEast.lng)/2)},getSouthWest:function(){return this._southWest},getNorthEast:function(){return this._northEast},getNorthWest:function(){return new L.LatLng(this._northEast.la
 t,this._southWest.lng,!0)},getSouthEast:function(){return new L.LatLng(this._southWest.lat,this._northEast.lng,!0)},contains:function(a){var b=this._southWest,c=this._northEast,d,e;return a instanceof L.LatLngBounds?(d=a.getSouthWest(),e=a.getNorthEast()):d=e=a,d.lat>=b.lat&&e.lat<=c.lat&&d.lng>=b.lng&&e.lng<=c.lng},intersects:function(a){var b=this._southWest,c=this._northEast,d=a.getSouthWest(),e=a.getNorthEast(),f=e.lat>=b.lat&&d.lat<=c.lat,g=e.lng>=b.lng&&d.lng<=c.lng;return f&&g},toBBoxString:function(){var a=this._southWest,b=this._northEast;return[a.lng,a.lat,b.lng,b.lat].join(",")}}),L.Projection={},L.Projection.SphericalMercator={MAX_LATITUDE:85.0511287798,project:function(a){var b=L.LatLng.DEG_TO_RAD,c=this.MAX_LATITUDE,d=Math.max(Math.min(c,a.lat),-c),e=a.lng*b,f=d*b;return f=Math.log(Math.tan(Math.PI/4+f/2)),new L.Point(e,f)},unproject:function(a,b){var c=L.LatLng.RAD_TO_DEG,d=a.x*c,e=(2*Math.atan(Math.exp(a.y))-Math.PI/2)*c;return new L.LatLng(e,d,b)}},L.Project
 ion.LonLat={project:function(a){return new L.Point(a.lng,a.lat)},unproject:function(a,b){return new L.LatLng(a.y,a.x,b)}},L.CRS={latLngToPoint:function(a,b){var c=this.projection.project(a);return this.transformation._transform(c,b)},pointToLatLng:function(a,b,c){var d=this.transformation.untransform(a,b);return this.projection.unproject(d,c)},project:function(a){return this.projection.project(a)}},L.CRS.EPSG3857=L.Util.extend({},L.CRS,{code:"EPSG:3857",projection:L.Projection.SphericalMercator,transformation:new L.Transformation(.5/Math.PI,.5,-0.5/Math.PI,.5),project:function(a){var b=this.projection.project(a),c=6378137;return b.multiplyBy(c)}}),L.CRS.EPSG900913=L.Util.extend({},L.CRS.EPSG3857,{code:"EPSG:900913"}),L.CRS.EPSG4326=L.Util.extend({},L.CRS,{code:"EPSG:4326",projection:L.Projection.LonLat,transformation:new L.Transformation(1/360,.5,-1/360,.5)}),L.Map=L.Class.extend({includes:L.Mixin.Events,options:{crs:L.CRS.EPSG3857||L.CRS.EPSG4326,scale:function(a){return 25
 6*Math.pow(2,a)},center:null,zoom:null,layers:[],dragging:!0,touchZoom:L.Browser.touch&&!L.Browser.android,scrollWheelZoom:!L.Browser.touch,doubleClickZoom:!0,boxZoom:!0,zoomControl:!0,attributionControl:!0,fadeAnimation:L.DomUtil.TRANSITION&&!L.Browser.android,zoomAnimation:L.DomUtil.TRANSITION&&!L.Browser.android&&!L.Browser.mobileOpera,trackResize:!0,closePopupOnClick:!0,worldCopyJump:!0},initialize:function(a,b){L.Util.setOptions(this,b),this._container=L.DomUtil.get(a);if(this._container._leaflet)throw Error("Map container is already initialized.");this._container._leaflet=!0,this._initLayout(),L.DomEvent&&(this._initEvents(),L.Handler&&this._initInteraction(),L.Control&&this._initControls()),this.options.maxBounds&&this.setMaxBounds(this.options.maxBounds);var c=this.options.center,d=this.options.zoom;c!==null&&d!==null&&this.setView(c,d,!0);var e=this.options.layers;e=e instanceof Array?e:[e],this._tileLayersNum=0,this._initLayers(e)},setView:function(a,b){return this
 ._resetView(a,this._limitZoom(b)),this},setZoom:function(a){return this.setView(this.getCenter(),a)},zoomIn:function(){return this.setZoom(this._zoom+1)},zoomOut:function(){return this.setZoom(this._zoom-1)},fitBounds:function(a){var b=this.getBoundsZoom(a);return this.setView(a.getCenter(),b)},fitWorld:function(){var a=new L.LatLng(-60,-170),b=new L.LatLng(85,179);return this.fitBounds(new L.LatLngBounds(a,b))},panTo:function(a){return this.setView(a,this._zoom)},panBy:function(a){return this.fire("movestart"),this._rawPanBy(a),this.fire("move"),this.fire("moveend"),this},setMaxBounds:function(a){this.options.maxBounds=a;if(!a)return this._boundsMinZoom=null,this;var b=this.getBoundsZoom(a,!0);return this._boundsMinZoom=b,this._loaded&&(this._zoom<b?this.setView(a.getCenter(),b):this.panInsideBounds(a)),this},panInsideBounds:function(a){var b=this.getBounds(),c=this.project(b.getSouthWest()),d=this.project(b.getNorthEast()),e=this.project(a.getSouthWest()),f=this.project(a.
 getNorthEast()),g=0,h=0;return d.y<f.y&&(h=f.y-d.y),d.x>f.x&&(g=f.x-d.x),c.y>e.y&&(h=e.y-c.y),c.x<e.x&&(g=e.x-c.x),this.panBy(new L.Point(g,h,!0))},addLayer:function(a,b){var c=L.Util.stamp(a);if(this._layers[c])return this;this._layers[c]=a,a.options&&!isNaN(a.options.maxZoom)&&(this._layersMaxZoom=Math.max(this._layersMaxZoom||0,a.options.maxZoom)),a.options&&!isNaN(a.options.minZoom)&&(this._layersMinZoom=Math.min(this._layersMinZoom||Infinity,a.options.minZoom)),this.options.zoomAnimation&&L.TileLayer&&a instanceof L.TileLayer&&(this._tileLayersNum++,a.on("load",this._onTileLayerLoad,this)),this.attributionControl&&a.getAttribution&&this.attributionControl.addAttribution(a.getAttribution());var d=function(){a.onAdd(this,b),this.fire("layeradd",{layer:a})};return this._loaded?d.call(this):this.on("load",d,this),this},removeLayer:function(a){var b=L.Util.stamp(a);return this._layers[b]&&(a.onRemove(this),delete this._layers[b],this.options.zoomAnimation&&L.TileLayer&&a ins
 tanceof L.TileLayer&&(this._tileLayersNum--,a.off("load",this._onTileLayerLoad,this)),this.attributionControl&&a.getAttribution&&this.attributionControl.removeAttribution(a.getAttribution()),this.fire("layerremove",{layer:a})),this},hasLayer:function(a){var b=L.Util.stamp(a);return this._layers.hasOwnProperty(b)},invalidateSize:function(){var a=this.getSize();return this._sizeChanged=!0,this.options.maxBounds&&this.setMaxBounds(this.options.maxBounds),this._loaded?(this._rawPanBy(a.subtract(this.getSize()).divideBy(2,!0)),this.fire("move"),clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(L.Util.bind(function(){this.fire("moveend")},this),200),this):this},getCenter:function(a){var b=this.getSize().divideBy(2),c=this._getTopLeftPoint().add(b);return this.unproject(c,this._zoom,a)},getZoom:function(){return this._zoom},getBounds:function(){var a=this.getPixelBounds(),b=this.unproject(new L.Point(a.min.x,a.max.y),this._zoom,!0),c=this.unproject(new L.Point(a.max.x,a.min.
 y),this._zoom,!0);return new L.LatLngBounds(b,c)},getMinZoom:function(){var a=this.options.minZoom||0,b=this._layersMinZoom||0,c=this._boundsMinZoom||0;return Math.max(a,b,c)},getMaxZoom:function(){var a=isNaN(this.options.maxZoom)?Infinity:this.options.maxZoom,b=this._layersMaxZoom||Infinity;return Math.min(a,b)},getBoundsZoom:function(a,b){var c=this.getSize(),d=this.options.minZoom||0,e=this.getMaxZoom(),f=a.getNorthEast(),g=a.getSouthWest(),h,i,j,k=!0;b&&d--;do d++,i=this.project(f,d),j=this.project(g,d),h=new L.Point(i.x-j.x,j.y-i.y),b?k=h.x<c.x||h.y<c.y:k=h.x<=c.x&&h.y<=c.y;while(k&&d<=e);return k&&b?null:b?d:d-1},getSize:function(){if(!this._size||this._sizeChanged)this._size=new L.Point(this._container.clientWidth,this._container.clientHeight),this._sizeChanged=!1;return this._size},getPixelBounds:function(){var a=this._getTopLeftPoint(),b=this.getSize();return new L.Bounds(a,a.add(b))},getPixelOrigin:function(){return this._initialTopLeftPoint},getPanes:function(){r
 eturn this._panes},mouseEventToContainerPoint:function(a){return L.DomEvent.getMousePosition(a,this._container)},mouseEventToLayerPoint:function(a){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(a))},mouseEventToLatLng:function(a){return this.layerPointToLatLng(this.mouseEventToLayerPoint(a))},containerPointToLayerPoint:function(a){return a.subtract(L.DomUtil.getPosition(this._mapPane))},layerPointToContainerPoint:function(a){return a.add(L.DomUtil.getPosition(this._mapPane))},layerPointToLatLng:function(a){return this.unproject(a.add(this._initialTopLeftPoint))},latLngToLayerPoint:function(a){return this.project(a)._round()._subtract(this._initialTopLeftPoint)},project:function(a,b){return b=typeof b=="undefined"?this._zoom:b,this.options.crs.latLngToPoint(a,this.options.scale(b))},unproject:function(a,b,c){return b=typeof b=="undefined"?this._zoom:b,this.options.crs.pointToLatLng(a,this.options.scale(b),c)},_initLayout:function(){var a=this._contain
 er;a.innerHTML="",a.className+=" leaflet-container",this.options.fadeAnimation&&(a.className+=" leaflet-fade-anim");var b=L.DomUtil.getStyle(a,"position");b!=="absolute"&&b!=="relative"&&(a.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var a=this._panes={};this._mapPane=a.mapPane=this._createPane("leaflet-map-pane",this._container),this._tilePane=a.tilePane=this._createPane("leaflet-tile-pane",this._mapPane),this._objectsPane=a.objectsPane=this._createPane("leaflet-objects-pane",this._mapPane),a.shadowPane=this._createPane("leaflet-shadow-pane"),a.overlayPane=this._createPane("leaflet-overlay-pane"),a.markerPane=this._createPane("leaflet-marker-pane"),a.popupPane=this._createPane("leaflet-popup-pane")},_createPane:function(a,b){return L.DomUtil.create("div",a,b||this._objectsPane)},_resetView:function(a,b,c,d){var e=this._zoom!==b;d||(this.fire("movestart"),e&&this.fire("zoomstart")),this._zoom=b,this._initia
 lTopLeftPoint=this._getNewTopLeftPoint(a);if(!c)L.DomUtil.setPosition(this._mapPane,new L.Point(0,0));else{var f=L.DomUtil.getPosition(this._mapPane);this._initialTopLeftPoint._add(f)}this._tileLayersToLoad=this._tileLayersNum,this.fire("viewreset",{hard:!c}),this.fire("move"),(e||d)&&this.fire("zoomend"),this.fire("moveend"),this._loaded||(this._loaded=!0,this.fire("load"))},_initLayers:function(a){this._layers={};var b,c;for(b=0,c=a.length;b<c;b++)this.addLayer(a[b])},_initControls:function(){this.options.zoomControl&&this.addControl(new L.Control.Zoom),this.options.attributionControl&&(this.attributionControl=new L.Control.Attribution,this.addControl(this.attributionControl))},_rawPanBy:function(a){var b=L.DomUtil.getPosition(this._mapPane);L.DomUtil.setPosition(this._mapPane,b.subtract(a))},_initEvents:function(){L.DomEvent.addListener(this._container,"click",this._onMouseClick,this);var a=["dblclick","mousedown","mouseenter","mouseleave","mousemove","contextmenu"],b,c;f
 or(b=0,c=a.length;b<c;b++)L.DomEvent.addListener(this._container,a[b],this._fireMouseEvent,this);this.options.trackResize&&L.DomEvent.addListener(window,"resize",this._onResize,this)},_onResize:function(){L.Util.requestAnimFrame(this.invalidateSize,this,!1,this._container)},_onMouseClick:function(a){if(!this._loaded||this.dragging&&this.dragging.moved())return;this.fire("pre"+a.type),this._fireMouseEvent(a)},_fireMouseEvent:function(a){if(!this._loaded)return;var b=a.type;b=b==="mouseenter"?"mouseover":b==="mouseleave"?"mouseout":b;if(!this.hasEventListeners(b))return;b==="contextmenu"&&L.DomEvent.preventDefault(a),this.fire(b,{latlng:this.mouseEventToLatLng(a),layerPoint:this.mouseEventToLayerPoint(a)})},_initInteraction:function(){var a={dragging:L.Map.Drag,touchZoom:L.Map.TouchZoom,doubleClickZoom:L.Map.DoubleClickZoom,scrollWheelZoom:L.Map.ScrollWheelZoom,boxZoom:L.Map.BoxZoom},b;for(b in a)a.hasOwnProperty(b)&&a[b]&&(this[b]=new a[b](this),this.options[b]&&this[b].enabl
 e())},_onTileLayerLoad:function(){this._tileLayersToLoad--,this._tileLayersNum&&!this._tileLayersToLoad&&this._tileBg&&(clearTimeout(this._clearTileBgTimer),this._clearTileBgTimer=setTimeout(L.Util.bind(this._clearTileBg,this),500))},_getTopLeftPoint:function(){if(!this._loaded)throw Error("Set map center and zoom first.");var a=L.DomUtil.getPosition(this._mapPane);return this._initialTopLeftPoint.subtract(a)},_getNewTopLeftPoint:function(a){var b=this.getSize().divideBy(2);return this.project(a).subtract(b).round()},_limitZoom:function(a){var b=this.getMinZoom(),c=this.getMaxZoom();return Math.max(b,Math.min(c,a))}}),L.Projection.Mercator={MAX_LATITUDE:85.0840591556,R_MINOR:6356752.3142,R_MAJOR:6378137,project:function(a){var b=L.LatLng.DEG_TO_RAD,c=this.MAX_LATITUDE,d=Math.max(Math.min(c,a.lat),-c),e=this.R_MAJOR,f=this.R_MINOR,g=a.lng*b*e,h=d*b,i=f/e,j=Math.sqrt(1-i*i),k=j*Math.sin(h);k=Math.pow((1-k)/(1+k),j*.5);var l=Math.tan(.5*(Math.PI*.5-h))/k;return h=-f*Math.log(l)
 ,new L.Point(g,h)},unproject:function(a,b){var c=L.LatLng.RAD_TO_DEG,d=this.R_MAJOR,e=this.R_MINOR,f=a.x*c/d,g=e/d,h=Math.sqrt(1-g*g),i=Math.exp(-a.y/e),j=Math.PI/2-2*Math.atan(i),k=15,l=1e-7,m=k,n=.1,o;while(Math.abs(n)>l&&--m>0)o=h*Math.sin(j),n=Math.PI/2-2*Math.atan(i*Math.pow((1-o)/(1+o),.5*h))-j,j+=n;return new L.LatLng(j*c,f,b)}},L.CRS.EPSG3395=L.Util.extend({},L.CRS,{code:"EPSG:3395",projection:L.Projection.Mercator,transformation:function(){var a=L.Projection.Mercator,b=a.R_MAJOR,c=a.R_MINOR;return new L.Transformation(.5/(Math.PI*b),.5,-0.5/(Math.PI*c),.5)}()}),L.TileLayer=L.Class.extend({includes:L.Mixin.Events,options:{minZoom:0,maxZoom:18,tileSize:256,subdomains:"abc",errorTileUrl:"",attribution:"",opacity:1,scheme:"xyz",continuousWorld:!1,noWrap:!1,zoomOffset:0,zoomReverse:!1,unloadInvisibleTiles:L.Browser.mobile,updateWhenIdle:L.Browser.mobile,reuseTiles:!1},initialize:function(a,b,c){L.Util.setOptions(this,b),this._url=a,this._urlParams=c,typeof this.options.s
 ubdomains=="string"&&(this.options.subdomains=this.options.subdomains.split(""))},onAdd:function(a,b){this._map=a,this._insertAtTheBottom=b,this._initContainer(),this._createTileProto(),a.on("viewreset",this._resetCallback,this),this.options.updateWhenIdle?a.on("moveend",this._update,this):(this._limitedUpdate=L.Util.limitExecByInterval(this._update,150,this),a.on("move",this._limitedUpdate,this)),this._reset(),this._update()},onRemove:function(a){this._map.getPanes().tilePane.removeChild(this._container),this._container=null,this._map.off("viewreset",this._resetCallback,this),this.options.updateWhenIdle?this._map.off("moveend",this._update,this):this._map.off("move",this._limitedUpdate,this)},getAttribution:function(){return this.options.attribution},setOpacity:function(a){this.options.opacity=a,this._setOpacity(a);if(L.Browser.webkit)for(var b in this._tiles)this._tiles.hasOwnProperty(b)&&(this._tiles[b].style.webkitTransform+=" translate(0,0)")},_setOpacity:function(a){a<
 1&&L.DomUtil.setOpacity(this._container,a)},_initContainer:function(){var a=this._map.getPanes().tilePane,b=a.firstChild;if(!this._container||a.empty)this._container=L.DomUtil.create("div","leaflet-layer"),this._insertAtTheBottom&&b?a.insertBefore(this._container,b):a.appendChild(this._container),this._setOpacity(this.options.opacity)},_resetCallback:function(a){this._reset(a.hard)},_reset:function(a){var b;for(b in this._tiles)this._tiles.hasOwnProperty(b)&&this.fire("tileunload",{tile:this._tiles[b]});this._tiles={},this.options.reuseTiles&&(this._unusedTiles=[]),a&&this._container&&(this._container.innerHTML=""),this._initContainer()},_update:function(){var a=this._map.getPixelBounds(),b=this._map.getZoom(),c=this.options.tileSize;if(b>this.options.maxZoom||b<this.options.minZoom)return;var d=new L.Point(Math.floor(a.min.x/c),Math.floor(a.min.y/c)),e=new L.Point(Math.floor(a.max.x/c),Math.floor(a.max.y/c)),f=new L.Bounds(d,e);this._addTilesFromCenterOut(f),(this.options.u
 nloadInvisibleTiles||this.options.reuseTiles)&&this._removeOtherTiles(f)},_addTilesFromCenterOut:function(a){var b=[],c=a.getCenter();for(var d=a.min.y;d<=a.max.y;d++)for(var e=a.min.x;e<=a.max.x;e++){if(e+":"+d in this._tiles)continue;b.push(new L.Point(e,d))}b.sort(function(a,b){return a.distanceTo(c)-b.distanceTo(c)});var f=document.createDocumentFragment();this._tilesToLoad=b.length;for(var g=0,h=this._tilesToLoad;g<h;g++)this._addTile(b[g],f);this._container.appendChild(f)},_removeOtherTiles:function(a){var b,c,d,e,f;for(e in this._tiles)if(this._tiles.hasOwnProperty(e)){b=e.split(":"),c=parseInt(b[0],10),d=parseInt(b[1],10);if(c<a.min.x||c>a.max.x||d<a.min.y||d>a.max.y)f=this._tiles[e],this.fire("tileunload",{tile:f,url:f.src}),f.parentNode===this._container&&this._container.removeChild(f),this.options.reuseTiles&&this._unusedTiles.push(this._tiles[e]),f.src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=",delete this._tiles[e]}},_addTile:function(a,b){var 
 c=this._getTilePos(a),d=this._map.getZoom(),e=a.x+":"+a.y,f=Math.pow(2,this._getOffsetZoom(d));if(!this.options.continuousWorld){if(!this.options.noWrap)a.x=(a.x%f+f)%f;else if(a.x<0||a.x>=f){this._tilesToLoad--;return}if(a.y<0||a.y>=f){this._tilesToLoad--;return}}var g=this._getTile();L.DomUtil.setPosition(g,c),this._tiles[e]=g,this.options.scheme==="tms"&&(a.y=f-a.y-1),this._loadTile(g,a,d),b.appendChild(g)},_getOffsetZoom:function(a){return a=this.options.zoomReverse?this.options.maxZoom-a:a,a+this.options.zoomOffset},_getTilePos:function(a){var b=this._map.getPixelOrigin(),c=this.options.tileSize;return a.multiplyBy(c).subtract(b)},getTileUrl:function(a,b){var c=this.options.subdomains,d=this.options.subdomains[(a.x+a.y)%c.length];return L.Util.template(this._url,L.Util.extend({s:d,z:this._getOffsetZoom(b),x:a.x,y:a.y},this._urlParams))},_createTileProto:function(){this._tileImg=L.DomUtil.create("img","leaflet-tile"),this._tileImg.galleryimg="no";var a=this.options.tileS
 ize;this._tileImg.style.width=a+"px",this._tileImg.style.height=a+"px"},_getTile:function(){if(this.options.reuseTiles&&this._unusedTiles.length>0){var a=this._unusedTiles.pop();return this._resetTile(a),a}return this._createTile()},_resetTile:function(a){},_createTile:function(){var a=this._tileImg.cloneNode(!1);return a.onselectstart=a.onmousemove=L.Util.falseFn,a},_loadTile:function(a,b,c){a._layer=this,a.onload=this._tileOnLoad,a.onerror=this._tileOnError,a.src=this.getTileUrl(b,c)},_tileOnLoad:function(a){var b=this._layer;this.className+=" leaflet-tile-loaded",b.fire("tileload",{tile:this,url:this.src}),b._tilesToLoad--,b._tilesToLoad||b.fire("load")},_tileOnError:function(a){var b=this._layer;b.fire("tileerror",{tile:this,url:this.src});var c=b.options.errorTileUrl;c&&(this.src=c)}}),L.TileLayer.WMS=L.TileLayer.extend({defaultWmsParams:{service:"WMS",request:"GetMap",version:"1.1.1",layers:"",styles:"",format:"image/jpeg",transparent:!1},initialize:function(a,b){this.
 _url=a,this.wmsParams=L.Util.extend({},this.defaultWmsParams),this.wmsParams.width=this.wmsParams.height=this.options.tileSize;for(var c in b)this.options.hasOwnProperty(c)||(this.wmsParams[c]=b[c]);L.Util.setOptions(this,b)},onAdd:function(a){var b=parseFloat(this.wmsParams.version)<1.3?"srs":"crs";this.wmsParams[b]=a.options.crs.code,L.TileLayer.prototype.onAdd.call(this,a)},getTileUrl:function(a,b){var c=this.options.tileSize,d=a.multiplyBy(c),e=d.add(new L.Point(c,c)),f=this._map.unproject(d,this._zoom,!0),g=this._map.unproject(e,this._zoom,!0),h=this._map.options.crs.project(f),i=this._map.options.crs.project(g),j=[h.x,i.y,i.x,h.y].join(",");return this._url+L.Util.getParamString(this.wmsParams)+"&bbox="+j}}),L.TileLayer.Canvas=L.TileLayer.extend({options:{async:!1},initialize:function(a){L.Util.setOptions(this,a)},redraw:function(){for(var a in this._tiles){var b=this._tiles[a];this._redrawTile(b)}},_redrawTile:function(a){this.drawTile(a,a._tilePoint,a._zoom)},_create
 TileProto:function(){this._canvasProto=L.DomUtil.create("canvas","leaflet-tile");var a=this.options.tileSize;this._canvasProto.width=a,this._canvasProto.height=a},_createTile:function(){var a=this._canvasProto.cloneNode(!1);return a.onselectstart=a.onmousemove=L.Util.falseFn,a},_loadTile:function(a,b,c){a._layer=this,a._tilePoint=b,a._zoom=c,this.drawTile(a,b,c),this.options.async||this.tileDrawn(a)},drawTile:function(a,b,c){},tileDrawn:function(a){this._tileOnLoad.call(a)}}),L.ImageOverlay=L.Class.extend({includes:L.Mixin.Events,initialize:function(a,b){this._url=a,this._bounds=b},onAdd:function(a){this._map=a,this._image||this._initImage(),a.getPanes().overlayPane.appendChild(this._image),a.on("viewreset",this._reset,this),this._reset()},onRemove:function(a){a.getPanes().overlayPane.removeChild(this._image),a.off("viewreset",this._reset,this)},_initImage:function(){this._image=L.DomUtil.create("img","leaflet-image-layer"),this._image.style.visibility="hidden",L.Util.extend
 (this._image,{galleryimg:"no",onselectstart:L.Util.falseFn,onmousemove:L.Util.falseFn,onload:L.Util.bind(this._onImageLoad,this),src:this._url})},_reset:function(){var a=this._map.latLngToLayerPoint(this._bounds.getNorthWest()),b=this._map.latLngToLayerPoint(this._bounds.getSouthEast()),c=b.subtract(a);L.DomUtil.setPosition(this._image,a),this._image.style.width=c.x+"px",this._image.style.height=c.y+"px"},_onImageLoad:function(){this._image.style.visibility="",this.fire("load")}}),L.Icon=L.Class.extend({iconUrl:L.ROOT_URL+"images/marker.png",shadowUrl:L.ROOT_URL+"images/marker-shadow.png",iconSize:new L.Point(25,41),shadowSize:new L.Point(41,41),iconAnchor:new L.Point(13,41),popupAnchor:new L.Point(0,-33),initialize:function(a){a&&(this.iconUrl=a)},createIcon:function(){return this._createIcon("icon")},createShadow:function(){return this._createIcon("shadow")},_createIcon:function(a){var b=this[a+"Size"],c=this[a+"Url"];if(!c&&a==="shadow")return null;var d;return c?d=this._
 createImg(c):d=this._createDiv(),d.className="leaflet-marker-"+a,d.style.marginLeft=-this.iconAnchor.x+"px",d.style.marginTop=-this.iconAnchor.y+"px",b&&(d.style.width=b.x+"px",d.style.height=b.y+"px"),d},_createImg:function(a){var b;return L.Browser.ie6?(b=document.createElement("div"),b.style.filter='progid:DXImageTransform.Microsoft.AlphaImageLoader(src="'+a+'")'):(b=document.createElement("img"),b.src=a),b},_createDiv:function(){return document.createElement("div")}}),L.Marker=L.Class.extend({includes:L.Mixin.Events,options:{icon:new L.Icon,title:"",clickable:!0,draggable:!1,zIndexOffset:0},initialize:function(a,b){L.Util.setOptions(this,b),this._latlng=a},onAdd:function(a){this._map=a,this._initIcon(),a.on("viewreset",this._reset,this),this._reset()},onRemove:function(a){this._removeIcon(),this.closePopup&&this.closePopup(),this._map=null,a.off("viewreset",this._reset,this)},getLatLng:function(){return this._latlng},setLatLng:function(a){this._latlng=a,this._icon&&(this
 ._reset(),this._popup&&this._popup.setLatLng(this._latlng))},setZIndexOffset:function(a){this.options.zIndexOffset=a,this._icon&&this._reset()},setIcon:function(a){this._map&&this._removeIcon(),this.options.icon=a,this._map&&(this._initIcon(),this._reset())},_initIcon:function(){this._icon||(this._icon=this.options.icon.createIcon(),this.options.title&&(this._icon.title=this.options.title),this._initInteraction()),this._shadow||(this._shadow=this.options.icon.createShadow()),this._map._panes.markerPane.appendChild(this._icon),this._shadow&&this._map._panes.shadowPane.appendChild(this._shadow)},_removeIcon:function(){this._map._panes.markerPane.removeChild(this._icon),this._shadow&&this._map._panes.shadowPane.removeChild(this._shadow),this._icon=this._shadow=null},_reset:function(){var a=this._map.latLngToLayerPoint(this._latlng).round();L.DomUtil.setPosition(this._icon,a),this._shadow&&L.DomUtil.setPosition(this._shadow,a),this._icon.style.zIndex=a.y+this.options.zIndexOffse
 t},_initInteraction:function(){if(this.options.clickable){this._icon.className+=" leaflet-clickable",L.DomEvent.addListener(this._icon,"click",this._onMouseClick,this);var a=["dblclick","mousedown","mouseover","mouseout"];for(var b=0;b<a.length;b++)L.DomEvent.addListener(this._icon,a[b],this._fireMouseEvent,this)}L.Handler.MarkerDrag&&(this.dragging=new L.Handler.MarkerDrag(this),this.options.draggable&&this.dragging.enable())},_onMouseClick:function(a){L.DomEvent.stopPropagation(a);if(this.dragging&&this.dragging.moved())return;this.fire(a.type)},_fireMouseEvent:function(a){this.fire(a.type),L.DomEvent.stopPropagation(a)}}),L.Popup=L.Class.extend({includes:L.Mixin.Events,options:{minWidth:50,maxWidth:300,autoPan:!0,closeButton:!0,offset:new L.Point(0,2),autoPanPadding:new L.Point(5,5),className:""},initialize:function(a,b){L.Util.setOptions(this,a),this._source=b},onAdd:function(a){this._map=a,this._container||this._initLayout(),this._updateContent(),this._container.style.o
 pacity="0",this._map._panes.popupPane.appendChild(this._container),this._map.on("viewreset",this._updatePosition,this),this._map.options.closePopupOnClick&&this._map.on("preclick",this._close,this),this._update(),this._container.style.opacity="1",this._opened=!0},onRemove:function(a){a._panes.popupPane.removeChild(this._container),L.Util.falseFn(this._container.offsetWidth),a.off("viewreset",this._updatePosition,this),a.off("click",this._close,this),this._container.style.opacity="0",this._opened=!1},setLatLng:function(a){return this._latlng=a,this._opened&&this._update(),this},setContent:function(a){return this._content=a,this._opened&&this._update(),this},_close:function(){this._opened&&this._map.closePopup()},_initLayout:function(){this._container=L.DomUtil.create("div","leaflet-popup "+this.options.className),this.options.closeButton&&(this._closeButton=L.DomUtil.create("a","leaflet-popup-close-button",this._container),this._closeButton.href="#close",L.DomEvent.addListene
 r(this._closeButton,"click",this._onCloseButtonClick,this)),this._wrapper=L.DomUtil.create("div","leaflet-popup-content-wrapper",this._container),L.DomEvent.disableClickPropagation(this._wrapper),this._contentNode=L.DomUtil.create("div","leaflet-popup-content",this._wrapper),this._tipContainer=L.DomUtil.create("div","leaflet-popup-tip-container",this._container),this._tip=L.DomUtil.create("div","leaflet-popup-tip",this._tipContainer)},_update:function(){this._container.style.visibility="hidden",this._updateContent(),this._updateLayout(),this._updatePosition(),this._container.style.visibility="",this._adjustPan()},_updateContent:function(){if(!this._content)return;typeof this._content=="string"?this._contentNode.innerHTML=this._content:(this._contentNode.innerHTML="",this._contentNode.appendChild(this._content))},_updateLayout:function(){this._container.style.width="",this._container.style.whiteSpace="nowrap";var a=this._container.offsetWidth;this._container.style.width=(a>th
 is.options.maxWidth?this.options.maxWidth:a<this.options.minWidth?this.options.minWidth:a)+"px",this._container.style.whiteSpace="",this._containerWidth=this._container.offsetWidth},_updatePosition:function(){var a=this._map.latLngToLayerPoint(this._latlng);this._containerBottom=-a.y-this.options.offset.y,this._containerLeft=a.x-Math.round(this._containerWidth/2)+this.options.offset.x,this._container.style.bottom=this._containerBottom+"px",this._container.style.left=this._containerLeft+"px"},_adjustPan:function(){if(!this.options.autoPan)return;var a=this._container.offsetHeight,b=new L.Point(this._containerLeft,-a-this._containerBottom),c=this._map.layerPointToContainerPoint(b),d=new L.Point(0,0),e=this.options.autoPanPadding,f=this._map.getSize();c.x<0&&(d.x=c.x-e.x),c.x+this._containerWidth>f.x&&(d.x=c.x+this._containerWidth-f.x+e.x),c.y<0&&(d.y=c.y-e.y),c.y+a>f.y&&(d.y=c.y+a-f.y+e.y),(d.x||d.y)&&this._map.panBy(d)},_onCloseButtonClick:function(a){this._close(),L.DomEvent
 .stop(a)}}),L.Marker.include({openPopup:function(){return this._popup.setLatLng(this._latlng),this._map&&this._map.openPopup(this._popup),this},closePopup:function(){return this._popup&&this._popup._close(),this},bindPopup:function(a,b){return b=L.Util.extend({offset:this.options.icon.popupAnchor},b),this._popup||this.on("click",this.openPopup,this),this._popup=new L.Popup(b,this),this._popup.setContent(a),this},unbindPopup:function(){return this._popup&&(this._popup=null,this.off("click",this.openPopup)),this}}),L.Map.include({openPopup:function(a){return this.closePopup(),this._popup=a,this.addLayer(a),this.fire("popupopen",{popup:this._popup}),this},closePopup:function(){return this._popup&&(this.removeLayer(this._popup),this.fire("popupclose",{popup:this._popup}),this._popup=null),this}}),L.LayerGroup=L.Class.extend({initialize:function(a){this._layers={};if(a)for(var b=0,c=a.length;b<c;b++)this.addLayer(a[b])},addLayer:function(a){var b=L.Util.stamp(a);return this._laye
 rs[b]=a,this._map&&this._map.addLayer(a),this},removeLayer:function(a){var b=L.Util.stamp(a);return delete this._layers[b],this._map&&this._map.removeLayer(a),this},clearLayers:function(){return this._iterateLayers(this.removeLayer,this),this},invoke:function(a){var b=Array.prototype.slice.call(arguments,1),c,d;for(c in this._layers)this._layers.hasOwnProperty(c)&&(d=this._layers[c],d[a]&&d[a].apply(d,b));return this},onAdd:function(a){this._map=a,this._iterateLayers(a.addLayer,a)},onRemove:function(a){this._iterateLayers(a.removeLayer,a),delete this._map},_iterateLayers:function(a,b){for(var c in this._layers)this._layers.hasOwnProperty(c)&&a.call(b,this._layers[c])}}),L.FeatureGroup=L.LayerGroup.extend({includes:L.Mixin.Events,addLayer:function(a){this._initEvents(a),L.LayerGroup.prototype.addLayer.call(this,a),this._popupContent&&a.bindPopup&&a.bindPopup(this._popupContent)},bindPopup:function(a){return this._popupContent=a,this.invoke("bindPopup",a)},setStyle:function(a)
 {return this.invoke("setStyle",a)},_events:["click","dblclick","mouseover","mouseout"],_initEvents:function(a){for(var b=0,c=this._events.length;b<c;b++)a.on(this._events[b],this._propagateEvent,this)},_propagateEvent:function(a){a.layer=a.target,a.target=this,this.fire(a.type,a)}}),L.Path=L.Class.extend({includes:[L.Mixin.Events],statics:{CLIP_PADDING:.5},options:{stroke:!0,color:"#0033ff",weight:5,opacity:.5,fill:!1,fillColor:null,fillOpacity:.2,clickable:!0,updateOnMoveEnd:!0},initialize:function(a){L.Util.setOptions(this,a)},onAdd:function(a){this._map=a,this._initElements(),this._initEvents(),this.projectLatlngs(),this._updatePath(),a.on("viewreset",this.projectLatlngs,this),this._updateTrigger=this.options.updateOnMoveEnd?"moveend":"viewreset",a.on(this._updateTrigger,this._updatePath,this)},onRemove:function(a){this._map=null,a._pathRoot.removeChild(this._container),a.off("viewreset",this.projectLatlngs,this),a.off(this._updateTrigger,this._updatePath,this)},projectLa
 tlngs:function(){},setStyle:function(a){return L.Util.setOptions(this,a),this._container&&this._updateStyle(),this},_redraw:function(){this._map&&(this.projectLatlngs(),this._updatePath())}}),L.Map.include({_updatePathViewport:function(){var a=L.Path.CLIP_PADDING,b=this.getSize(),c=L.DomUtil.getPosition(this._mapPane),d=c.multiplyBy(-1).subtract(b.multiplyBy(a)),e=d.add(b.multiplyBy(1+a*2));this._pathViewport=new L.Bounds(d,e)}}),L.Path.SVG_NS="http://www.w3.org/2000/svg",L.Browser.svg=!!document.createElementNS&&!!document.createElementNS(L.Path.SVG_NS,"svg").createSVGRect,L.Path=L.Path.extend({statics:{SVG:L.Browser.svg,_createElement:function(a){return document.createElementNS(L.Path.SVG_NS,a)}},getPathString:function(){},_initElements:function(){this._map._initPathRoot(),this._initPath(),this._initStyle()},_initPath:function(){this._container=L.Path._createElement("g"),this._path=L.Path._createElement("path"),this._container.appendChild(this._path),this._map._pathRoot.ap
 pendChild(this._container)},_initStyle:function(){this.options.stroke&&(this._path.setAttribute("stroke-linejoin","round"),this._path.setAttribute("stroke-linecap","round")),this.options.fill?this._path.setAttribute("fill-rule","evenodd"):this._path.setAttribute("fill","none"),this._updateStyle()},_updateStyle:function(){this.options.stroke&&(this._path.setAttribute("stroke",this.options.color),this._path.setAttribute("stroke-opacity",this.options.opacity),this._path.setAttribute("stroke-width",this.options.weight)),this.options.fill&&(this._path.setAttribute("fill",this.options.fillColor||this.options.color),this._path.setAttribute("fill-opacity",this.options.fillOpacity))},_updatePath:function(){var a=this.getPathString();a||(a="M0 0"),this._path.setAttribute("d",a)},_initEvents:function(){if(this.options.clickable){L.Browser.vml||this._path.setAttribute("class","leaflet-clickable"),L.DomEvent.addListener(this._container,"click",this._onMouseClick,this);var a=["dblclick","
 mousedown","mouseover","mouseout","mousemove"];for(var b=0;b<a.length;b++)L.DomEvent.addListener(this._container,a[b],this._fireMouseEvent,this)}},_onMouseClick:function(a){if(this._map.dragging&&this._map.dragging.moved())return;this._fireMouseEvent(a)},_fireMouseEvent:function(a){if(!this.hasEventListeners(a.type))return;this.fire(a.type,{latlng:this._map.mouseEventToLatLng(a),layerPoint:this._map.mouseEventToLayerPoint(a)}),L.DomEvent.stopPropagation(a)}}),L.Map.include({_initPathRoot:function(){this._pathRoot||(this._pathRoot=L.Path._createElement("svg"),this._panes.overlayPane.appendChild(this._pathRoot),this.on("moveend",this._updateSvgViewport),this._updateSvgViewport())},_updateSvgViewport:function(){this._updatePathViewport();var a=this._pathViewport,b=a.min,c=a.max,d=c.x-b.x,e=c.y-b.y,f=this._pathRoot,g=this._panes.overlayPane;L.Browser.webkit&&g.removeChild(f),L.DomUtil.setPosition(f,b),f.setAttribute("width",d),f.setAttribute("height",e),f.setAttribute("viewBox",
 [b.x,b.y,d,e].join(" ")),L.Browser.webkit&&g.appendChild(f)}}),L.Path.include({bindPopup:function(a,b){if(!this._popup||this._popup.options!==b)this._popup=new L.Popup(b,this);return this._popup.setContent(a),this._openPopupAdded||(this.on("click",this._openPopup,this),this._openPopupAdded=!0),this},_openPopup:function(a){this._popup.setLatLng(a.latlng),this._map.openPopup(this._popup)}}),L.Browser.vml=function(){var a=document.createElement("div"),b;return a.innerHTML='<v:shape adj="1"/>',b=a.firstChild,b.style.behavior="url(#default#VML)",b&&typeof b.adj=="object"}(),L.Path=L.Browser.svg||!L.Browser.vml?L.Path:L.Path.extend({statics:{VML:!0,CLIP_PADDING:.02,_createElement:function(){try{return document.namespaces.add("lvml","urn:schemas-microsoft-com:vml"),function(a){return document.createElement("<lvml:"+a+' class="lvml">')}}catch(a){return function(a){return document.createElement("<"+a+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}()},_initPath:function(){t
 his._container=L.Path._createElement("shape"),this._container.className+=" leaflet-vml-shape"+(this.options.clickable?" leaflet-clickable":""),this._container.coordsize="1 1",this._path=L.Path._createElement("path"),this._container.appendChild(this._path),this._map._pathRoot.appendChild(this._container)},_initStyle:function(){this.options.stroke?(this._stroke=L.Path._createElement("stroke"),this._stroke.endcap="round",this._container.appendChild(this._stroke)):this._container.stroked=!1,this.options.fill?(this._container.filled=!0,this._fill=L.Path._createElement("fill"),this._container.appendChild(this._fill)):this._container.filled=!1,this._updateStyle()},_updateStyle:function(){this.options.stroke&&(this._stroke.weight=this.options.weight+"px",this._stroke.color=this.options.color,this._stroke.opacity=this.options.opacity),this.options.fill&&(this._fill.color=this.options.fillColor||this.options.color,this._fill.opacity=this.options.fillOpacity)},_updatePath:function(){th
 is._container.style.display="none",this._path.v=this.getPathString()+" ",this._container.style.display=""}}),L.Map.include(L.Browser.svg||!L.Browser.vml?{}:{_initPathRoot:function(){this._pathRoot||(this._pathRoot=document.createElement("div"),this._pathRoot.className="leaflet-vml-container",this._panes.overlayPane.appendChild(this._pathRoot),this.on("moveend",this._updatePathViewport),this._updatePathViewport())}}),L.Browser.canvas=function(){return!!document.createElement("canvas").getContext}(),L.Path=L.Path.SVG&&!window.L_PREFER_CANVAS||!L.Browser.canvas?L.Path:L.Path.extend({statics:{CANVAS:!0,SVG:!1},options:{updateOnMoveEnd:!0},_initElements:function(){this._map._initPathRoot(),this._ctx=this._map._canvasCtx},_updateStyle:function(){this.options.stroke&&(this._ctx.lineWidth=this.options.weight,this._ctx.strokeStyle=this.options.color),this.options.fill&&(this._ctx.fillStyle=this.options.fillColor||this.options.color)},_drawPath:function(){var a,b,c,d,e,f;this._ctx.beg
 inPath();for(a=0,c=this._parts.length;a<c;a++){for(b=0,d=this._parts[a].length;b<d;b++)e=this._parts[a][b],f=(b===0?"move":"line")+"To",this._ctx[f](e.x,e.y);this instanceof L.Polygon&&this._ctx.closePath()}},_checkIfEmpty:function(){return!this._parts.length},_updatePath:function(){if(this._checkIfEmpty())return;this._drawPath(),this._ctx.save(),this._updateStyle();var a=this.options.opacity,b=this.options.fillOpacity;this.options.fill&&(b<1&&(this._ctx.globalAlpha=b),this._ctx.fill()),this.options.stroke&&(a<1&&(this._ctx.globalAlpha=a),this._ctx.stroke()),this._ctx.restore()},_initEvents:function(){this.options.clickable&&this._map.on("click",this._onClick,this)},_onClick:function(a){this._containsPoint(a.layerPoint)&&this.fire("click",a)},onRemove:function(a){a.off("viewreset",this._projectLatlngs,this),a.off(this._updateTrigger,this._updatePath,this),a.fire(this._updateTrigger)}}),L.Map.include(L.Path.SVG&&!window.L_PREFER_CANVAS||!L.Browser.canvas?{}:{_initPathRoot:fun
 ction(){var a=this._pathRoot,b;a||(a=this._pathRoot=document.createElement("canvas"),a.style.position="absolute",b=this._canvasCtx=a.getContext("2d"),b.lineCap="round",b.lineJoin="round",this._panes.overlayPane.appendChild(a),this.on("moveend",this._updateCanvasViewport),this._updateCanvasViewport())},_updateCanvasViewport:function(){this._updatePathViewport();var a=this._pathViewport,b=a.min,c=a.max.subtract(b),d=this._pathRoot;L.DomUtil.setPosition(d,b),d.width=c.x,d.height=c.y,d.getContext("2d").translate(-b.x,-b.y)}}),L.LineUtil={simplify:function(a,b){if(!b||!a.length)return a.slice();var c=b*b;return a=this._reducePoints(a,c),a=this._simplifyDP(a,c),a},pointToSegmentDistance:function(a,b,c){return Math.sqrt(this._sqClosestPointOnSegment(a,b,c,!0))},closestPointOnSegment:function(a,b,c){return this._sqClosestPointOnSegment(a,b,c)},_simplifyDP:function(a,b){var c=a.length,d=typeof Uint8Array!="undefined"?Uint8Array:Array,e=new d(c);e[0]=e[c-1]=1,this._simplifyDPStep(a,e,
 b,0,c-1);var f,g=[];for(f=0;f<c;f++)e[f]&&g.push(a[f]);return g},_simplifyDPStep:function(a,b,c,d,e){var f=0,g,h,i;for(h=d+1;h<=e-1;h++)i=this._sqClosestPointOnSegment(a[h],a[d],a[e],!0),i>f&&(g=h,f=i);f>c&&(b[g]=1,this._simplifyDPStep(a,b,c,d,g),this._simplifyDPStep(a,b,c,g,e))},_reducePoints:function(a,b){var c=[a[0]];for(var d=1,e=0,f=a.length;d<f;d++)this._sqDist(a[d],a[e])>b&&(c.push(a[d]),e=d);return e<f-1&&c.push(a[f-1]),c},clipSegment:function(a,b,c,d){var e=c.min,f=c.max,g=d?this._lastCode:this._getBitCode(a,c),h=this._getBitCode(b,c);this._lastCode=h;for(;;){if(!(g|h))return[a,b];if(g&h)return!1;var i=g||h,j=this._getEdgeIntersection(a,b,i,c),k=this._getBitCode(j,c);i===g?(a=j,g=k):(b=j,h=k)}},_getEdgeIntersection:function(a,b,c,d){var e=b.x-a.x,f=b.y-a.y,g=d.min,h=d.max;if(c&8)return new L.Point(a.x+e*(h.y-a.y)/f,h.y);if(c&4)return new L.Point(a.x+e*(g.y-a.y)/f,g.y);if(c&2)return new L.Point(h.x,a.y+f*(h.x-a.x)/e);if(c&1)return new L.Point(g.x,a.y+f*(g.x-a.x)/e)},
 _getBitCode:function(a,b){var c=0;return a.x<b.min.x?c|=1:a.x>b.max.x&&(c|=2),a.y<b.min.y?c|=4:a.y>b.max.y&&(c|=8),c},_sqDist:function(a,b){var c=b.x-a.x,d=b.y-a.y;return c*c+d*d},_sqClosestPointOnSegment:function(a,b,c,d){var e=b.x,f=b.y,g=c.x-e,h=c.y-f,i=g*g+h*h,j;return i>0&&(j=((a.x-e)*g+(a.y-f)*h)/i,j>1?(e=c.x,f=c.y):j>0&&(e+=g*j,f+=h*j)),g=a.x-e,h=a.y-f,d?g*g+h*h:new L.Point(e,f)}},L.Polyline=L.Path.extend({initialize:function(a,b){L.Path.prototype.initialize.call(this,b),this._latlngs=a},options:{smoothFactor:1,noClip:!1,updateOnMoveEnd:!0},projectLatlngs:function(){this._originalPoints=[];for(var a=0,b=this._latlngs.length;a<b;a++)this._originalPoints[a]=this._map.latLngToLayerPoint(this._latlngs[a])},getPathString:function(){for(var a=0,b=this._parts.length,c="";a<b;a++)c+=this._getPathPartStr(this._parts[a]);return c},getLatLngs:function(){return this._latlngs},setLatLngs:function(a){return this._latlngs=a,this._redraw(),this},addLatLng:function(a){return this._lat
 lngs.push(a),this._redraw(),this},spliceLatLngs:function(a,b){var c=[].splice.apply(this._latlngs,arguments);return this._redraw(),c},closestLayerPoint:function(a){var b=Infinity,c=this._parts,d,e,f=null;for(var g=0,h=c.length;g<h;g++){var i=c[g];for(var j=1,k=i.length;j<k;j++){d=i[j-1],e=i[j];var l=L.LineUtil._sqClosestPointOnSegment(a,d,e);l._sqDist<b&&(b=l._sqDist,f=l)}}return f&&(f.distance=Math.sqrt(b)),f},getBounds:function(){var a=new L.LatLngBounds,b=this.getLatLngs();for(var c=0,d=b.length;c<d;c++)a.extend(b[c]);return a},_getPathPartStr:function(a){var b=L.Path.VML;for(var c=0,d=a.length,e="",f;c<d;c++)f=a[c],b&&f._round(),e+=(c?"L":"M")+f.x+" "+f.y;return e},_clipPoints:function(){var a=this._originalPoints,b=a.length,c,d,e;if(this.options.noClip){this._parts=[a];return}this._parts=[];var f=this._parts,g=this._map._pathViewport,h=L.LineUtil;for(c=0,d=0;c<b-1;c++){e=h.clipSegment(a[c],a[c+1],g,c);if(!e)continue;f[d]=f[d]||[],f[d].push(e[0]);if(e[1]!==a[c+1]||c===b-
 2)f[d].push(e[1]),d++}},_simplifyPoints:function(){var a=this._parts,b=L.LineUtil;for(var c=0,d=a.length;c<d;c++)a[c]=b.simplify(a[c],this.options.smoothFactor)},_updatePath:function(){this._clipPoints(),this._simplifyPoints(),L.Path.prototype._updatePath.call(this)}}),L.PolyUtil={},L.PolyUtil.clipPolygon=function(a,b){var c=b.min,d=b.max,e,f=[1,4,2,8],g,h,i,j,k,l,m,n,o=L.LineUtil;for(g=0,l=a.length;g<l;g++)a[g]._code=o._getBitCode(a[g],b);for(i=0;i<4;i++){m=f[i],e=[];for(g=0,l=a.length,h=l-1;g<l;h=g++)j=a[g],k=a[h],j._code&m?k._code&m||(n=o._getEdgeIntersection(k,j,m,b),n._code=o._getBitCode(n,b),e.push(n)):(k._code&m&&(n=o._getEdgeIntersection(k,j,m,b),n._code=o._getBitCode(n,b),e.push(n)),e.push(j));a=e}return a},L.Polygon=L.Polyline.extend({options:{fill:!0},initialize:function(a,b){L.Polyline.prototype.initialize.call(this,a,b),a&&a[0]instanceof Array&&(this._latlngs=a[0],this._holes=a.slice(1))},projectLatlngs:function(){L.Polyline.prototype.projectLatlngs.call(this),t
 his._holePoints=[];if(!this._holes)return;for(var a=0,b=this._holes.length,c;a<b;a++){this._holePoints[a]=[];for(var d=0,e=this._holes[a].length;d<e;d++)this._holePoints[a][d]=this._map.latLngToLayerPoint(this._holes[a][d])}},_clipPoints:function(){var a=this._originalPoints,b=[];this._parts=[a].concat(this._holePoints);if(this.options.noClip)return;for(var c=0,d=this._parts.length;c<d;c++){var e=L.PolyUtil.clipPolygon(this._parts[c],this._map._pathViewport);if(!e.length)continue;b.push(e)}this._parts=b},_getPathPartStr:function(a){var b=L.Polyline.prototype._getPathPartStr.call(this,a);return b+(L.Browser.svg?"z":"x")}}),function(){function a(a){return L.FeatureGroup.extend({initialize:function(a,b){this._layers={},this._options=b,this.setLatLngs(a)},setLatLngs:function(b){var c=0,d=b.length;this._iterateLayers(function(a){c<d?a.setLatLngs(b[c++]):this.removeLayer(a)},this);while(c<d)this.addLayer(new a(b[c++],this._options))}})}L.MultiPolyline=a(L.Polyline),L.MultiPolygon=
 a(L.Polygon)}(),L.Circle=L.Path.extend({initialize:function(a,b,c){L.Path.prototype.initialize.call(this,c),this._latlng=a,this._mRadius=b},options:{fill:!0},setLatLng:function(a){return this._latlng=a,this._redraw(),this},setRadius:function(a){return this._mRadius=a,this._redraw(),this},projectLatlngs:function(){var a=40075017,b=a*Math.cos(L.LatLng.DEG_TO_RAD*this._latlng.lat),c=this._mRadius/b*360,d=new L.LatLng(this._latlng.lat,this._latlng.lng-c,!0),e=this._map.latLngToLayerPoint(d);this._point=this._map.latLngToLayerPoint(this._latlng),this._radius=Math.round(this._point.x-e.x)},getPathString:function(){var a=this._point,b=this._radius;return this._checkIfEmpty()?"":L.Browser.svg?"M"+a.x+","+(a.y-b)+"A"+b+","+b+",0,1,1,"+(a.x-.1)+","+(a.y-b)+" z":(a._round(),b=Math.round(b),"AL "+a.x+","+a.y+" "+b+","+b+" 0,"+23592600)},_checkIfEmpty:function(){var a=this._map._pathViewport,b=this._radius,c=this._point;return c.x-b>a.max.x||c.y-b>a.max.y||c.x+b<a.min.x||c.y+b<a.min.y}})
 ,L.CircleMarker=L.Circle.extend({options:{radius:10,weight:2},initialize:function(a,b){L.Circle.prototype.initialize.call(this,a,null,b),this._radius=this.options.radius},projectLatlngs:function(){this._point=this._map.latLngToLayerPoint(this._latlng)},setRadius:function(a){return this._radius=a,this._redraw(),this}}),L.Polyline.include(L.Path.CANVAS?{_containsPoint:function(a,b){var c,d,e,f,g,h,i,j=this.options.weight/2;L.Browser.touch&&(j+=10);for(c=0,f=this._parts.length;c<f;c++){i=this._parts[c];for(d=0,g=i.length,e=g-1;d<g;e=d++){if(!b&&d===0)continue;h=L.LineUtil.pointToSegmentDistance(a,i[e],i[d]);if(h<=j)return!0}}return!1}}:{}),L.Polygon.include(L.Path.CANVAS?{_containsPoint:function(a){var b=!1,c,d,e,f,g,h,i,j;if(L.Polyline.prototype._containsPoint.call(this,a,!0))return!0;for(f=0,i=this._parts.length;f<i;f++){c=this._parts[f];for(g=0,j=c.length,h=j-1;g<j;h=g++)d=c[g],e=c[h],d.y>a.y!=e.y>a.y&&a.x<(e.x-d.x)*(a.y-d.y)/(e.y-d.y)+d.x&&(b=!b)}return b}}:{}),L.Circle.inc
 lude(L.Path.CANVAS?{_drawPath:function(){var a=this._point;this._ctx.beginPath(),this._ctx.arc(a.x,a.y,this._radius,0,Math.PI*2)},_containsPoint:function(a){var b=this._point,c=this.options.stroke?this.options.weight/2:0;return a.distanceTo(b)<=this._radius+c}}:{}),L.GeoJSON=L.FeatureGroup.extend({initialize:function(a,b){L.Util.setOptions(this,b),this._geojson=a,this._layers={},a&&this.addGeoJSON(a)},addGeoJSON:function(a){if(a.features){for(var b=0,c=a.features.length;b<c;b++)this.addGeoJSON(a.features[b]);return}var d=a.type==="Feature",e=d?a.geometry:a,f=L.GeoJSON.geometryToLayer(e,this.options.pointToLayer);this.fire("featureparse",{layer:f,properties:a.properties,geometryType:e.type,bbox:a.bbox,id:a.id}),this.addLayer(f)}}),L.Util.extend(L.GeoJSON,{geometryToLayer:function(a,b){var c=a.coordinates,d,e,f,g,h,i=[];switch(a.type){case"Point":return d=this.coordsToLatLng(c),b?b(d):new L.Marker(d);case"MultiPoint":for(f=0,g=c.length;f<g;f++)d=this.coordsToLatLng(c[f]),h=b?b
 (d):new L.Marker(d),i.push(h);return new L.FeatureGroup(i);case"LineString":return e=this.coordsToLatLngs(c),new L.Polyline(e);case"Polygon":return e=this.coordsToLatLngs(c,1),new L.Polygon(e);case"MultiLineString":return e=this.coordsToLatLngs(c,1),new L.MultiPolyline(e);case"MultiPolygon":return e=this.coordsToLatLngs(c,2),new L.MultiPolygon(e);case"GeometryCollection":for(f=0,g=a.geometries.length;f<g;f++)h=this.geometryToLayer(a.geometries[f],b),i.push(h);return new L.FeatureGroup(i);default:throw Error("Invalid GeoJSON object.")}},coordsToLatLng:function(a,b){var c=parseFloat(a[b?0:1]),d=parseFloat(a[b?1:0]);return new L.LatLng(c,d,!0)},coordsToLatLngs:function(a,b,c){var d,e=[],f,g=a.length;for(f=0;f<g;f++)d=b?this.coordsToLatLngs(a[f],b-1,c):this.coordsToLatLng(a[f],c),e.push(d);return e}}),L.DomEvent={addListener:function(a,b,c,d){var e=L.Util.stamp(c),f="_leaflet_"+b+e;if(a[f])return;var g=function(b){return c.call(d||a,b||L.DomEvent._getEvent())};if(L.Browser.touch
 &&b==="dblclick"&&this.addDoubleTapListener)this.addDoubleTapListener(a,g,e);else if("addEventListener"in a)if(b==="mousewheel")a.addEventListener("DOMMouseScroll",g,!1),a.addEventListener(b,g,!1);else if(b==="mouseenter"||b==="mouseleave"){var h=g,i=b==="mouseenter"?"mouseover":"mouseout";g=function(b){if(!L.DomEvent._checkMouse(a,b))return;return h(b)},a.addEventListener(i,g,!1)}else a.addEventListener(b,g,!1);else"attachEvent"in a&&a.attachEvent("on"+b,g);a[f]=g},removeListener:function(a,b,c){var d=L.Util.stamp(c),e="_leaflet_"+b+d,f=a[e];if(!f)return;L.Browser.touch&&b==="dblclick"&&this.removeDoubleTapListener?this.removeDoubleTapListener(a,d):"removeEventListener"in a?b==="mousewheel"?(a.removeEventListener("DOMMouseScroll",f,!1),a.removeEventListener(b,f,!1)):b==="mouseenter"||b==="mouseleave"?a.removeEventListener(b==="mouseenter"?"mouseover":"mouseout",f,!1):a.removeEventListener(b,f,!1):"detachEvent"in a&&a.detachEvent("on"+b,f),a[e]=null},_checkMouse:function(a,b
 ){var c=b.relatedTarget;if(!c)return!0;try{while(c&&c!==a)c=c.parentNode}catch(d){return!1}return c!==a},_getEvent:function(){var a=window.event;if(!a){var b=arguments.callee.caller;while(b){a=b.arguments[0];if(a&&window.Event===a.constructor)break;b=b.caller}}return a},stopPropagation:function(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0},disableClickPropagation:function(a){L.DomEvent.addListener(a,L.Draggable.START,L.DomEvent.stopPropagation),L.DomEvent.addListener(a,"click",L.DomEvent.stopPropagation),L.DomEvent.addListener(a,"dblclick",L.DomEvent.stopPropagation)},preventDefault:function(a){a.preventDefault?a.preventDefault():a.returnValue=!1},stop:function(a){L.DomEvent.preventDefault(a),L.DomEvent.stopPropagation(a)},getMousePosition:function(a,b){var c=a.pageX?a.pageX:a.clientX+document.body.scrollLeft+document.documentElement.scrollLeft,d=a.pageY?a.pageY:a.clientY+document.body.scrollTop+document.documentElement.scrollTop,e=new L.Point(c,d);return b?e.s
 ubtract(L.DomUtil.getViewportOffset(b)):e},getWheelDelta:function(a){var b=0;return a.wheelDelta&&(b=a.wheelDelta/120),a.detail&&(b=-a.detail/3),b}},L.Draggable=L.Class.extend({includes:L.Mixin.Events,statics:{START:L.Browser.touch?"touchstart":"mousedown",END:L.Browser.touch?"touchend":"mouseup",MOVE:L.Browser.touch?"touchmove":"mousemove",TAP_TOLERANCE:15},initialize:function(a,b){this._element=a,this._dragStartTarget=b||a},enable:function(){if(this._enabled)return;L.DomEvent.addListener(this._dragStartTarget,L.Draggable.START,this._onDown,this),this._enabled=!0},disable:function(){if(!this._enabled)return;L.DomEvent.removeListener(this._dragStartTarget,L.Draggable.START,this._onDown),this._enabled=!1},_onDown:function(a){if(!L.Browser.touch&&a.shiftKey||a.which!==1&&a.button!==1&&!a.touches)return;if(a.touches&&a.touches.length>1)return;var b=a.touches&&a.touches.length===1?a.touches[0]:a,c=b.target;L.DomEvent.preventDefault(a),L.Browser.touch&&c.tagName.toLowerCase()==="
 a"&&(c.className+=" leaflet-active"),this._moved=!1;if(this._moving)return;L.Browser.touch||(L.DomUtil.disableTextSelection(),this._setMovingCursor()),this._startPos=this._newPos=L.DomUtil.getPosition(this._element),this._startPoint=new L.Point(b.clientX,b.clientY),L.DomEvent.addListener(document,L.Draggable.MOVE,this._onMove,this),L.DomEvent.addListener(document,L.Draggable.END,this._onUp,this)},_onMove:function(a){if(a.touches&&a.touches.length>1)return;L.DomEvent.preventDefault(a);var b=a.touches&&a.touches.length===1?a.touches[0]:a;this._moved||(this.fire("dragstart"),this._moved=!0),this._moving=!0;var c=new L.Point(b.clientX,b.clientY);this._newPos=this._startPos.add(c).subtract(this._startPoint),L.Util.requestAnimFrame(this._updatePosition,this,!0,this._dragStartTarget)},_updatePosition:function(){this.fire("predrag"),L.DomUtil.setPosition(this._element,this._newPos),this.fire("drag")},_onUp:function(a){if(a.changedTouches){var b=a.changedTouches[0],c=b.target,d=this.
 _newPos&&this._newPos.distanceTo(this._startPos)||0;c.tagName.toLowerCase()==="a"&&(c.className=c.className.replace(" leaflet-active","")),d<L.Draggable.TAP_TOLERANCE&&this._simulateEvent("click",b)}L.Browser.touch||(L.DomUtil.enableTextSelection(),this._restoreCursor()),L.DomEvent.removeListener(document,L.Draggable.MOVE,this._onMove),L.DomEvent.removeListener(document,L.Draggable.END,this._onUp),this._moved&&this.fire("dragend"),this._moving=!1},_setMovingCursor:function(){this._bodyCursor=document.body.style.cursor,document.body.style.cursor="move"},_restoreCursor:function(){document.body.style.cursor=this._bodyCursor},_simulateEvent:function(a,b){var c=document.createEvent("MouseEvents");c.initMouseEvent(a,!0,!0,window,1,b.screenX,b.screenY,b.clientX,b.clientY,!1,!1,!1,!1,0,null),b.target.dispatchEvent(c)}}),L.Handler=L.Class.extend({initialize:function(a){this._map=a},enable:function(){if(this._enabled)return;this._enabled=!0,this.addHooks()},disable:function(){if(!this
 ._enabled)return;this._enabled=!1,this.removeHooks()},enabled:function(){return!!this._enabled}}),L.Map.Drag=L.Handler.extend({addHooks:function(){if(!this._draggable){this._draggable=new L.Draggable(this._map._mapPane,this._map._container),this._draggable.on("dragstart",this._onDragStart,this).on("drag",this._onDrag,this).on("dragend",this._onDragEnd,this);var a=this._map.options;a.worldCopyJump&&!a.continuousWorld&&(this._draggable.on("predrag",this._onPreDrag,this),this._map.on("viewreset",this._onViewReset,this))}this._draggable.enable()},removeHooks:function(){this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},_onDragStart:function(){this._map.fire("movestart").fire("dragstart")},_onDrag:function(){this._map.fire("move").fire("drag")},_onViewReset:function(){var a=this._map.getSize().divideBy(2),b=this._map.latLngToLayerPoint(new L.LatLng(0,0));this._initialWorldOffset=b.subtract(a)},_onPreDrag:function(){var a=this._map,b=a.opti
 ons.scale(a.getZoom()),c=Math.round(b/2),d=this._initialWorldOffset.x,e=this._draggable._newPos.x,f=(e-c+d)%b+c-d,g=(e+c+d)%b-c-d,h=Math.abs(f+d)<Math.abs(g+d)?f:g;this._draggable._newPos.x=h},_onDragEnd:function(){var a=this._map;a.fire("moveend").fire("dragend"),a.options.maxBounds&&L.Util.requestAnimFrame(this._panInsideMaxBounds,a,!0,a._container)},_panInsideMaxBounds:function(){this.panInsideBounds(this.options.maxBounds)}}),L.Map.DoubleClickZoom=L.Handler.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick)},_onDoubleClick:function(a){this.setView(a.latlng,this._zoom+1)}}),L.Map.ScrollWheelZoom=L.Handler.extend({addHooks:function(){L.DomEvent.addListener(this._map._container,"mousewheel",this._onWheelScroll,this),this._delta=0},removeHooks:function(){L.DomEvent.removeListener(this._map._container,"mousewheel",this._onWheelScroll)},_onWheelScroll:function(a){var b=L.DomEvent.getWhe
 elDelta(a);this._delta+=b,this._lastMousePos=this._map.mouseEventToContainerPoint(a),clearTimeout(this._timer),this._timer=setTimeout(L.Util.bind(this._performZoom,this),50),L.DomEvent.preventDefault(a)},_performZoom:function(){var a=this._map,b=Math.round(this._delta),c=a.getZoom();b=Math.max(Math.min(b,4),-4),b=a._limitZoom(c+b)-c,this._delta=0;if(!b)return;var d=this._getCenterForScrollWheelZoom(this._lastMousePos,b),e=c+b;a.setView(d,e)},_getCenterForScrollWheelZoom:function(a,b){var c=this._map,d=c.getPixelBounds().getCenter(),e=c.getSize().divideBy(2),f=a.subtract(e).multiplyBy(1-Math.pow(2,-b)),g=d.add(f);return c.unproject(g,c._zoom,!0)}}),L.Util.extend(L.DomEvent,{addDoubleTapListener:function(a,b,c){function k(a){if(a.touches.length!==1)return;var b=Date.now(),c=b-(d||b);g=a.touches[0],e=c>0&&c<=f,d=b}function l(a){e&&(g.type="dblclick",b(g),d=null)}var d,e=!1,f=250,g,h="_leaflet_",i="touchstart",j="touchend";a[h+i+c]=k,a[h+j+c]=l,a.addEventListener(i,k,!1),a.addEv
 entListener(j,l,!1)},removeDoubleTapListener:function(a,b){var c="_leaflet_";a.removeEventListener(a,a[c+"touchstart"+b],!1),a.removeEventListener(a,a[c+"touchend"+b],!1)}}),L.Map.TouchZoom=L.Handler.extend({addHooks:function(){L.DomEvent.addListener(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){L.DomEvent.removeListener(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(a){if(!a.touches||a.touches.length!==2||this._map._animatingZoom)return;var b=this._map.mouseEventToLayerPoint(a.touches[0]),c=this._map.mouseEventToLayerPoint(a.touches[1]),d=this._map.containerPointToLayerPoint(this._map.getSize().divideBy(2));this._startCenter=b.add(c).divideBy(2,!0),this._startDist=b.distanceTo(c),this._moved=!1,this._zooming=!0,this._centerOffset=d.subtract(this._startCenter),L.DomEvent.addListener(document,"touchmove",this._onTouchMove,this),L.DomEvent.addListener(document,"touchend",this._onTouchEnd,this),L.DomEvent.
 preventDefault(a)},_onTouchMove:function(a){if(!a.touches||a.touches.length!==2)return;this._moved||(this._map._mapPane.className+=" leaflet-zoom-anim",this._map.fire("zoomstart").fire("movestart")._prepareTileBg(),this._moved=!0);var b=this._map.mouseEventToLayerPoint(a.touches[0]),c=this._map.mouseEventToLayerPoint(a.touches[1]);this._scale=b.distanceTo(c)/this._startDist,this._delta=b.add(c).divideBy(2,!0).subtract(this._startCenter),this._map._tileBg.style.webkitTransform=[L.DomUtil.getTranslateString(this._delta),L.DomUtil.getScaleString(this._scale,this._startCenter)].join(" "),L.DomEvent.preventDefault(a)},_onTouchEnd:function(a){if(!this._moved||!this._zooming)return;this._zooming=!1;var b=this._map.getZoom(),c=Math.log(this._scale)/Math.LN2,d=c>0?Math.ceil(c):Math.floor(c),e=this._map._limitZoom(b+d),f=e-b,g=this._centerOffset.subtract(this._delta).divideBy(this._scale),h=this._map.getPixelOrigin().add(this._startCenter).add(g),i=this._map.unproject(h);L.DomEvent.re
 moveListener(document,"touchmove",this._onTouchMove),L.DomEvent.removeListener(document,"touchend",this._onTouchEnd);var j=Math.pow(2,f);this._map._runAnimation(i,e,j/this._scale,this._startCenter.add(g))}}),L.Map.BoxZoom=L.Handler.extend({initialize:function(a){this._map=a,this._container=a._container,this._pane=a._panes.overlayPane},addHooks:function(){L.DomEvent.addListener(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){L.DomEvent.removeListener(this._container,"mousedown",this._onMouseDown)},_onMouseDown:function(a){if(!a.shiftKey||a.which!==1&&a.button!==1)return!1;L.DomUtil.disableTextSelection(),this._startLayerPoint=this._map.mouseEventToLayerPoint(a),this._box=L.DomUtil.create("div","leaflet-zoom-box",this._pane),L.DomUtil.setPosition(this._box,this._startLayerPoint),this._container.style.cursor="crosshair",L.DomEvent.addListener(document,"mousemove",this._onMouseMove,this),L.DomEvent.addListener(document,"mouseup",this._onMouseUp,this),
 L.DomEvent.preventDefault(a)},_onMouseMove:function(a){var b=this._map.mouseEventToLayerPoint(a),c=b.x-this._startLayerPoint.x,d=b.y-this._startLayerPoint.y,e=Math.min(b.x,this._startLayerPoint.x),f=Math.min(b.y,this._startLayerPoint.y),g=new L.Point(e,f);L.DomUtil.setPosition(this._box,g),this._box.style.width=Math.abs(c)-4+"px",this._box.style.height=Math.abs(d)-4+"px"},_onMouseUp:function(a){this._pane.removeChild(this._box),this._container.style.cursor="",L.DomUtil.enableTextSelection(),L.DomEvent.removeListener(document,"mousemove",this._onMouseMove),L.DomEvent.removeListener(document,"mouseup",this._onMouseUp);var b=this._map.mouseEventToLayerPoint(a),c=new L.LatLngBounds(this._map.layerPointToLatLng(this._startLayerPoint),this._map.layerPointToLatLng(b));this._map.fitBounds(c)}}),L.Handler.MarkerDrag=L.Handler.extend({initialize:function(a){this._marker=a},addHooks:function(){var a=this._marker._icon;this._draggable||(this._draggable=new L.Draggable(a,a),this._draggab
 le.on("dragstart",this._onDragStart,this).on("drag",this._onDrag,this).on("dragend",this._onDragEnd,this)),this._draggable.enable()},removeHooks:function(){this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},_onDragStart:function(a){this._marker.closePopup().fire("movestart").fire("dragstart")},_onDrag:function(a){var b=L.DomUtil.getPosition(this._marker._icon);this._marker._shadow&&L.DomUtil.setPosition(this._marker._shadow,b),this._marker._latlng=this._marker._map.layerPointToLatLng(b),this._marker.fire("move").fire("drag")},_onDragEnd:function(){this._marker.fire("moveend").fire("dragend")}}),L.Control={},L.Control.Position={TOP_LEFT:"topLeft",TOP_RIGHT:"topRight",BOTTOM_LEFT:"bottomLeft",BOTTOM_RIGHT:"bottomRight"},L.Map.include({addControl:function(a){a.onAdd(this);var b=a.getPosition(),c=this._controlCorners[b],d=a.getContainer();return L.DomUtil.addClass(d,"leaflet-control"),b.indexOf("bottom")!==-1?c.insertBefore(d,c.firstChild
 ):c.appendChild(d),this},removeControl:function(a){var b=a.getPosition(),c=this._controlCorners[b],d=a.getContainer();return c.removeChild(d),a.onRemove&&a.onRemove(this),this},_initControlPos:function(){var a=this._controlCorners={},b="leaflet-",c=b+"top",d=b+"bottom",e=b+"left",f=b+"right",g=L.DomUtil.create("div",b+"control-container",this._container);L.Browser.touch&&(g.className+=" "+b+"big-buttons"),a.topLeft=L.DomUtil.create("div",c+" "+e,g),a.topRight=L.DomUtil.create("div",c+" "+f,g),a.bottomLeft=L.DomUtil.create("div",d+" "+e,g),a.bottomRight=L.DomUtil.create("div",d+" "+f,g)}}),L.Control.Zoom=L.Class.extend({onAdd:function(a){this._map=a,this._container=L.DomUtil.create("div","leaflet-control-zoom"),this._zoomInButton=this._createButton("Zoom in","leaflet-control-zoom-in",this._map.zoomIn,this._map),this._zoomOutButton=this._createButton("Zoom out","leaflet-control-zoom-out",this._map.zoomOut,this._map),this._container.appendChild(this._zoomInButton),this._contain
 er.appendChild(this._zoomOutButton)},getContainer:function(){return this._container},getPosition:function(){return L.Control.Position.TOP_LEFT},_createButton:function(a,b,c,d){var e=document.createElement("a");return e.href="#",e.title=a,e.className=b,L.Browser.touch||L.DomEvent.disableClickPropagation(e),L.DomEvent.addListener(e,"click",L.DomEvent.preventDefault),L.DomEvent.addListener(e,"click",c,d),e}}),L.Control.Attribution=L.Class.extend({initialize:function(a){this._prefix=a||'Powered by <a href="http://leaflet.cloudmade.com">Leaflet</a>',this._attributions={}},onAdd:function(a){this._container=L.DomUtil.create("div","leaflet-control-attribution"),L.DomEvent.disableClickPropagation(this._container),this._map=a,this._update()},getPosition:function(){return L.Control.Position.BOTTOM_RIGHT},getContainer:function(){return this._container},setPrefix:function(a){this._prefix=a,this._update()},addAttribution:function(a){if(!a)return;this._attributions[a]||(this._attributions[
 a]=0),this._attributions[a]++,this._update()},removeAttribution:function(a){if(!a)return;this._attributions[a]--,this._update()},_update:function(){if(!this._map)return;var a=[];for(var b in this._attributions)this._attributions.hasOwnProperty(b)&&a.push(b);var c=[];this._prefix&&c.push(this._prefix),a.length&&c.push(a.join(", ")),this._container.innerHTML=c.join(" — ")}}),L.Control.Layers=L.Class.extend({options:{collapsed:!0},initialize:function(a,b,c){L.Util.setOptions(this,c),this._layers={};for(var d in a)a.hasOwnProperty(d)&&this._addLayer(a[d],d);for(d in b)b.hasOwnProperty(d)&&this._addLayer(b[d],d,!0)},onAdd:function(a){this._map=a,this._initLayout(),this._update()},getContainer:function(){return this._container},getPosition:function(){return L.Control.Position.TOP_RIGHT},addBaseLayer:function(a,b){return this._addLayer(a,b),this._update(),this},addOverlay:function(a,b){return this._addLayer(a,b,!0),this._update(),this},removeLayer:function(a){var b=L.Util.sta
 mp(a);return delete this._layers[b],this._update(),this},_initLayout:function(){this._container=L.DomUtil.create("div","leaflet-control-layers"),L.Browser.touch||L.DomEvent.disableClickPropagation(this._container),this._form=L.DomUtil.create("form","leaflet-control-layers-list");if(this.options.collapsed){L.DomEvent.addListener(this._container,"mouseover",this._expand,this),L.DomEvent.addListener(this._container,"mouseout",this._collapse,this);var a=this._layersLink=L.DomUtil.create("a","leaflet-control-layers-toggle");a.href="#",a.title="Layers",L.Browser.touch?L.DomEvent.addListener(a,"click",this._expand,this):L.DomEvent.addListener(a,"focus",this._expand,this),this._map.on("movestart",this._collapse,this),this._container.appendChild(a)}else this._expand();this._baseLayersList=L.DomUtil.create("div","leaflet-control-layers-base",this._form),this._separator=L.DomUtil.create("div","leaflet-control-layers-separator",this._form),this._overlaysList=L.DomUtil.create("div","leaf
 let-control-layers-overlays",this._form),this._container.appendChild(this._form)},_addLayer:function(a,b,c){var d=L.Util.stamp(a);this._layers[d]={layer:a,name:b,overlay:c}},_update:function(){if(!this._container)return;this._baseLayersList.innerHTML="",this._overlaysList.innerHTML="";var a=!1,b=!1;for(var c in this._layers)if(this._layers.hasOwnProperty(c)){var d=this._layers[c];this._addItem(d),b=b||d.overlay,a=a||!d.overlay}this._separator.style.display=b&&a?"":"none"},_addItem:function(a,b){var c=document.createElement("label"),d=document.createElement("input");a.overlay||(d.name="leaflet-base-layers"),d.type=a.overlay?"checkbox":"radio",d.checked=this._map.hasLayer(a.layer),d.layerId=L.Util.stamp(a.layer),L.DomEvent.addListener(d,"click",this._onInputClick,this);var e=document.createTextNode(" "+a.name);c.appendChild(d),c.appendChild(e);var f=a.overlay?this._overlaysList:this._baseLayersList;f.appendChild(c)},_onInputClick:function(){var a,b,c,d=this._form.getElementsBy
 TagName("input"),e=d.length;for(a=0;a<e;a++)b=d[a],c=this._layers[b.layerId],b.checked?this._map.addLayer(c.layer,!c.overlay):this._map.removeLayer(c.layer)},_expand:function(){L.DomUtil.addClass(this._container,"leaflet-control-layers-expanded")},_collapse:function(){this._container.className=this._container.className.replace(" leaflet-control-layers-expanded","")}}),L.Transition=L.Class.extend({includes:L.Mixin.Events,statics:{CUSTOM_PROPS_SETTERS:{position:L.DomUtil.setPosition},implemented:function(){return L.Transition.NATIVE||L.Transition.TIMER}},options:{easing:"ease",duration:.5},_setProperty:function(a,b){var c=L.Transition.CUSTOM_PROPS_SETTERS;a in c?c[a](this._el,b):this._el.style[a]=b}}),L.Transition=L.Transition.extend({statics:function(){var a=L.DomUtil.TRANSITION,b=a==="webkitTransition"||a==="OTransition"?a+"End":"transitionend";return{NATIVE:!!a,TRANSITION:a,PROPERTY:a+"Property",DURATION:a+"Duration",EASING:a+"TimingFunction",END:b,CUSTOM_PROPS_PROPERTIES:{
 position:L.Browser.webkit?L.DomUtil.TRANSFORM:"top, left"}}}(),options:{fakeStepInterval:100},initialize:function(a,b){this._el=a,L.Util.setOptions(this,b),L.DomEvent.addListener(a,L.Transition.END,this._onTransitionEnd,this),this._onFakeStep=L.Util.bind(this._onFakeStep,this)},run:function(a){var b,c=[],d=L.Transition.CUSTOM_PROPS_PROPERTIES;for(b in a)a.hasOwnProperty(b)&&(b=d[b]?d[b]:b,b=this._dasherize(b),c.push(b));this._el.style[L.Transition.DURATION]=this.options.duration+"s",this._el.style[L.Transition.EASING]=this.options.easing,this._el.style[L.Transition.PROPERTY]=c.join(", ");for(b in a)a.hasOwnProperty(b)&&this._setProperty(b,a[b]);this._inProgress=!0,this.fire("start"),L.Transition.NATIVE?(clearInterval(this._timer),this._timer=setInterval(this._onFakeStep,this.options.fakeStepInterval)):this._onTransitionEnd()},_dasherize:function(){function b(a){return"-"+a.toLowerCase()}var a=/([A-Z])/g;return function(c){return c.replace(a,b)}}(),_onFakeStep:function(){this
 .fire("step")},_onTransitionEnd:function(){this._inProgress&&(this._inProgress=!1,clearInterval(this._timer),this._el.style[L.Transition.PROPERTY]="none",this.fire("step"),this.fire("end"))}}),L.Transition=L.Transition.NATIVE?L.Transition:L.Transition.extend({statics:{getTime:Date.now||function(){return+(new Date)},TIMER:!0,EASINGS:{ease:[.25,.1,.25,1],linear:[0,0,1,1],"ease-in":[.42,0,1,1],"ease-out":[0,0,.58,1],"ease-in-out":[.42,0,.58,1]},CUSTOM_PROPS_GETTERS:{position:L.DomUtil.getPosition},UNIT_RE:/^[\d\.]+(\D*)$/},options:{fps:50},initialize:function(a,b){this._el=a,L.Util.extend(this.options,b);var c=L.Transition.EASINGS[this.options.easing]||L.Transition.EASINGS.ease;this._p1=new L.Point(0,0),this._p2=new L.Point(c[0],c[1]),this._p3=new L.Point(c[2],c[3]),this._p4=new L.Point(1,1),this._step=L.Util.bind(this._step,this),this._interval=Math.round(1e3/this.options.fps)},run:function(a){this._props={};var b=L.Transition.CUSTOM_PROPS_GETTERS,c=L.Transition.UNIT_RE;this.f
 ire("start");for(var d in a)if(a.hasOwnProperty(d)){var e={};if(d in b)e.from=b[d](this._el);else{var f=this._el.style[d].match(c);e.from=parseFloat(f[0]),e.unit=f[1]}e.to=a[d],this._props[d]=e}clearInterval(this._timer),this._timer=setInterval(this._step,this._interval),this._startTime=L.Transition.getTime()},_step:function(){var a=L.Transition.getTime(),b=a-this._startTime,c=this.options.duration*1e3;b<c?this._runFrame(this._cubicBezier(b/c)):(this._runFrame(1),this._complete())},_runFrame:function(a){var b=L.Transition.CUSTOM_PROPS_SETTERS,c,d,e;for(c in this._props)this._props.hasOwnProperty(c)&&(d=this._props[c],c in b?(e=d.to.subtract(d.from).multiplyBy(a).add(d.from),b[c](this._el,e)):this._el.style[c]=(d.to-d.from)*a+d.from+d.unit);this.fire("step")},_complete:function(){clearInterval(this._timer),this.fire("end")},_cubicBezier:function(a){var b=Math.pow(1-a,3),c=3*Math.pow(1-a,2)*a,d=3*(1-a)*Math.pow(a,2),e=Math.pow(a,3),f=this._p1.multiplyBy(b),g=this._p2.multiplyB
 y(c),h=this._p3.multiplyBy(d),i=this._p4.multiplyBy(e);return f.add(g).add(h).add(i).y}}),L.Map.include(!L.Transition||!L.Transition.implemented()?{}:{setView:function(a,b,c){b=this._limitZoom(b);var d=this._zoom!==b;if(this._loaded&&!c&&this._layers){var e=this._getNewTopLeftPoint(a).subtract(this._getTopLeftPoint());a=new L.LatLng(a.lat,a.lng);var f=d?!!this._zoomToIfCenterInView&&this._zoomToIfCenterInView(a,b,e):this._panByIfClose(e);if(f)return this}return this._resetView(a,b),this},panBy:function(a){return!a.x&&!a.y?this:(this._panTransition||(this._panTransition=new L.Transition(this._mapPane,{duration:.3}),this._panTransition.on("step",this._onPanTransitionStep,this),this._panTransition.on("end",this._onPanTransitionEnd,this)),this.fire("movestart"),this._panTransition.run({position:L.DomUtil.getPosition(this._mapPane).subtract(a)}),this)},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){this.fire("moveend")},_panByIfClose:function(a)
 {return this._offsetIsWithinView(a)?(this.panBy(a),!0):!1},_offsetIsWithinView:function(a,b){var c=b||1,d=this.getSize();return Math.abs(a.x)<=d.x*c&&Math.abs(a.y)<=d.y*c}}),L.Map.include(L.DomUtil.TRANSITION?{_zoomToIfCenterInView:function(a,b,c){if(this._animatingZoom)return!0;if(!this.options.zoomAnimation)return!1;var d=b-this._zoom,e=Math.pow(2,d),f=c.divideBy(1-1/e);if(!this._offsetIsWithinView(f,1))return!1;this._mapPane.className+=" leaflet-zoom-anim",this.fire("movestart").fire("zoomstart");var g=this.containerPointToLayerPoint(this.getSize().divideBy(2)),h=g.add(f);return this._prepareTileBg(),this._runAnimation(a,b,e,h),!0},_runAnimation:function(a,b,c,d){this._animatingZoom=!0,this._animateToCenter=a,this._animateToZoom=b;var e=L.DomUtil.TRANSFORM;clearTimeout(this._clearTileBgTimer);if(L.Browser.gecko||window.opera)this._tileBg.style[e]+=" translate(0,0)";var f;L.Browser.android?(this._tileBg.style[e+"Origin"]=d.x+"px "+d.y+"px",f="scale("+c+")"):f=L.DomUtil.get
 ScaleString(c,d),L.Util.falseFn(this._tileBg.offsetWidth);var g={};g[e]=this._tileBg.style[e]+" "+f,this._tileBg.transition.run(g)},_prepareTileBg:function(){this._tileBg||(this._tileBg=this._createPane("leaflet-tile-pane",this._mapPane),this._tileBg.style.zIndex=1);var a=this._tilePane,b=this._tileBg;b.style[L.DomUtil.TRANSFORM]="",b.style.visibility="hidden",b.empty=!0,a.empty=!1,this._tilePane=this._panes.tilePane=b,this._tileBg=a,this._tileBg.transition||(this._tileBg.transition=new L.Transition(this._tileBg,{duration:.3,easing:"cubic-bezier(0.25,0.1,0.25,0.75)"}),this._tileBg.transition.on("end",this._onZoomTransitionEnd,this)),this._stopLoadingBgTiles()},_stopLoadingBgTiles:function(){var a=[].slice.call(this._tileBg.getElementsByTagName("img"));for(var b=0,c=a.length;b<c;b++)a[b].complete||(a[b].onload=L.Util.falseFn,a[b].onerror=L.Util.falseFn,a[b].src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=",a[b].parentNode.removeChild(a[b]),a[b]=null)},_onZoomTr
 ansitionEnd:function(){this._restoreTileFront(),L.Util.falseFn(this._tileBg.offsetWidth),this._resetView(this._animateToCenter,this._animateToZoom,!0,!0),this._mapPane.className=this._mapPane.className.replace(" leaflet-zoom-anim",""),this._animatingZoom=!1},_restoreTileFront:function(){this._tilePane.innerHTML="",this._tilePane.style.visibility="",this._tilePane.style.zIndex=2,this._tileBg.style.zIndex=1},_clearTileBg:function(){!this._animatingZoom&&!this.touchZoom._zooming&&(this._tileBg.innerHTML="")}}:{}),L.Map.include({locate:function(a){this._locationOptions=a=L.Util.extend({watch:!1,setView:!1,maxZoom:Infinity,timeout:1e4,maximumAge:0,enableHighAccuracy:!1},a);if(!navigator.geolocation)return this.fire("locationerror",{code:0,message:"Geolocation not supported."});var b=L.Util.bind(this._handleGeolocationResponse,this),c=L.Util.bind(this._handleGeolocationError,this);return a.watch?this._locationWatchId=navigator.geolocation.watchPosition(b,c,a):navigator.geolocation
 .getCurrentPosition(b,c,a),this},stopLocate:function(){navigator.geolocation&&navigator.geolocation.clearWatch(this._locationWatchId)},locateAndSetView:function(a,b){return b=L.Util.extend({maxZoom:a||Infinity,setView:!0},b),this.locate(b)},_handleGeolocationError:function(a){var b=a.code,c=b===1?"permission denied":b===2?"position unavailable":"timeout";this._locationOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:b,message:"Geolocation error: "+c+"."})},_handleGeolocationResponse:function(a){var b=180*a.coords.accuracy/4e7,c=b*2,d=a.coords.latitude,e=a.coords.longitude,f=new L.LatLng(d,e),g=new L.LatLng(d-b,e-c),h=new L.LatLng(d+b,e+c),i=new L.LatLngBounds(g,h);if(this._locationOptions.setView){var j=Math.min(this.getBoundsZoom(i),this._locationOptions.maxZoom);this.setView(f,j)}this.fire("locationfound",{latlng:f,bounds:i,accuracy:a.coords.accuracy})}});
\ No newline at end of file
diff --git a/ckan/public/scripts/vendor/recline/css/data-explorer.css b/ckan/public/scripts/vendor/recline/css/data-explorer.css
index b27e3f8..6cc5b0a 100644
--- a/ckan/public/scripts/vendor/recline/css/data-explorer.css
+++ b/ckan/public/scripts/vendor/recline/css/data-explorer.css
@@ -1,17 +1,24 @@
-.data-explorer .header .navigation,
-.data-explorer .header .navigation li,
-.data-explorer .header .pagination,
-.data-explorer .header .pagination form
+.recline-data-explorer .header .navigation,
+.recline-data-explorer .header .navigation li,
+.recline-data-explorer .header .pagination,
+.recline-data-explorer .header .pagination form
 {
   display: inline;
 }
 
-.data-explorer .header .navigation {
+.recline-data-explorer .header .navigation {
   float: left;
   margin-left: 0;
   padding-left: 0;
 }
 
+.recline-data-explorer .header .menu-right {
+  float: right;
+  margin-left: 5px;
+  padding-left: 5px;
+  border-left: solid 2px #ddd;
+}
+
 .header .recline-results-info {
   line-height: 28px;
   margin-left: 20px;
@@ -27,40 +34,44 @@
   margin-bottom: auto;
 }
 
-.header .recline-query-editor .text-query input {
-  float: left;  
-}
-
-.recline-query-editor .text-query .btn-group {
-  display: inline;
-  float:left;
-  margin-left:-2px;
-}
-
-.recline-query-editor .text-query .btn-group .dropdown-toggle {
-  -moz-border-radius:0px 3px 3px 0px;
-  -webkit-border-radius:0px 3px 3px 0px;
-  border-radius:0px 3px 3px 0px;
+.recline-query-editor .add-on {
+  float: left;
 }
 
-.recline-query-editor .text-query .btn-group ul {
-  margin-left:-110px;
+.header .recline-query-editor .pagination input {
+  width: 30px;
+  height: 18px;
+  padding: 2px 4px;
+  margin-top: -4px;
 }
 
 .header .recline-query-editor .pagination a {
   line-height: 26px;
+  padding: 0 6px;
 }
 
-.data-view-container {
+.header .recline-query-editor form button {
+  vertical-align: top;
+}
+
+.recline-data-explorer .data-view-container {
   display: block;
   clear: both;
 }
 
+.recline-filter-editor .filter-term .input-append a {
+  margin-left: -5px;
+}
+
+.recline-facet-viewer .facet-summary label {
+  display: inline;
+}
+
 /**********************************************************
   * Notifications
   *********************************************************/
 
-.notification-loader {
+.recline-data-explorer .notification-loader {
   width: 18px;
   margin-left: 5px;
   background: url(images/small-spinner.gif) no-repeat;
@@ -72,33 +83,33 @@
   * Data Table
   *********************************************************/
 
-.data-table .btn-group .dropdown-toggle {
+.recline-grid .btn-group .dropdown-toggle {
   padding: 1px 3px;
   line-height: auto;
 }
 
-.data-table-container {
+.recline-grid-container {
   overflow: auto;
   height: 550px;
 }
 
-.data-table {
+.recline-grid {
   border: 1px solid #ccc;
   width: 100%;
 }
 
-.data-table td, .data-table th {
+.recline-grid td, .recline-grid th {
   border-left: 1px solid #ccc;
   padding: 3px 4px;
   text-align: left;
 }
 
-.data-table tr td:first-child, .data-table tr th:first-child {
+.recline-grid tr td:first-child, .recline-grid tr th:first-child {
   width: 20px;
 }
 
 /* direct borrowing from twitter buttons */
-.data-table th,
+.recline-grid th,
 .transform-column-view .expression-preview-table-wrapper th
 {
   background-color: #e6e6e6;
@@ -137,27 +148,6 @@
   display: none;
 }
 
-.column-header-recon-stats-bar {
-  margin-top: 10px;
-  height: 4px;
-  background: #ddd;
-  border: 1px solid #ccc;
-  position: relative;
-  width: 100%;
-}
-
-.column-header-recon-stats-matched {
-  position: absolute;
-  height: 100%;
-  background: #282;
-}
-
-.column-header-recon-stats-blanks {
-  position: absolute;
-  height: 100%;
-  background: #3d3;
-}
-
 div.data-table-cell-content {
   line-height: 1.2;
   color: #222;
@@ -185,7 +175,7 @@ a.data-table-cell-edit:hover {
   background-position: -25px 0px;
 }
 
-.data-table td:hover .data-table-cell-edit {
+.recline-grid td:hover .data-table-cell-edit {
   visibility: visible;
 }
 
@@ -202,21 +192,6 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit {
   color: red;
 }
 
-/* TODO: not sure the rest of this is needed */
-.data-table-cell-editor, .data-table-topic-popup {
-  overflow: auto;
-  border: 1px solid #bcf;
-  background: #e3e9ff;
-  padding: 5px;
-  -moz-border-radius: 5px;
-  -webkit-border-radius: 5px;
-  border-radius: 5px;
-}
-
-.data-table-topic-popup-header {
-  padding: 0 0 5px;
-}
-
 .data-table-cell-editor-editor {
   overflow: hidden;
   display: block;
@@ -246,28 +221,6 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit {
   color: #999;
 }
 
-ul.sorting-dialog-blank-error-positions {
-  margin: 0;
-  padding: 5px;
-  height: 10em;
-  border: 1px solid #ccc;
-  -moz-border-radius: 3px;
-  -webkit-border-radius: 3px;
-  border-radius: 3px;
-}
-
-ul.sorting-dialog-blank-error-positions > li {
-  display: block;
-  border: 1px solid #ccc;
-  background: #eee;
-  padding: 5px;
-  margin: 2px;
-  cursor: move;
-  -moz-border-radius: 3px;
-  -webkit-border-radius: 3px;
-  border-radius: 3px;
-}
-
 
 /**********************************************************
   * Dialogs
@@ -429,8 +382,8 @@ td.expression-preview-value {
   * Read-only mode
   *********************************************************/
 
-.read-only .no-hidden .data-table tr td:first-child,
-.read-only .no-hidden .data-table tr th:first-child
+.read-only .no-hidden .recline-grid tr td:first-child,
+.read-only .no-hidden .recline-grid tr th:first-child
 {
   display: none;
 }
diff --git a/ckan/public/scripts/vendor/recline/css/map.css b/ckan/public/scripts/vendor/recline/css/map.css
new file mode 100644
index 0000000..c8adde7
--- /dev/null
+++ b/ckan/public/scripts/vendor/recline/css/map.css
@@ -0,0 +1,23 @@
+.data-map-container .map {
+  height: 500px;
+}
+
+/**********************************************************
+  * Editor
+  *********************************************************/
+
+.data-map-container .editor {
+  float: right;
+  width: 200px;
+  padding-left: 0px;
+  margin-left: 10px;
+}
+
+.data-map-container .editor form {
+  padding-left: 4px;
+}
+
+.data-map-container .editor select {
+	width: 100%;
+}
+
diff --git a/ckan/public/scripts/vendor/recline/recline.js b/ckan/public/scripts/vendor/recline/recline.js
index 217175e..2871c6d 100644
--- a/ckan/public/scripts/vendor/recline/recline.js
+++ b/ckan/public/scripts/vendor/recline/recline.js
@@ -72,17 +72,33 @@ this.recline.Model = this.recline.Model || {};
 
 (function($, my) {
 
-// ## A Dataset model
+// ## <a id="dataset">A Dataset model</a>
 //
 // A model has the following (non-Backbone) attributes:
 //
-// * fields: (aka columns) is a FieldList listing all the fields on this
-//   Dataset (this can be set explicitly, or, will be set by Dataset.fetch() or Dataset.query()
-// * currentDocuments: a DocumentList containing the Documents we have
-//   currently loaded for viewing (you update currentDocuments by calling query)
-// * docCount: total number of documents in this dataset
+// @property {FieldList} fields: (aka columns) is a `FieldList` listing all the
+// fields on this Dataset (this can be set explicitly, or, will be set by
+// Dataset.fetch() or Dataset.query()
+//
+// @property {DocumentList} currentDocuments: a `DocumentList` containing the
+// Documents we have currently loaded for viewing (updated by calling query
+// method)
+//
+// @property {number} docCount: total number of documents in this dataset
+//
+// @property {Backend} backend: the Backend (instance) for this Dataset
+//
+// @property {Query} queryState: `Query` object which stores current
+// queryState. queryState may be edited by other components (e.g. a query
+// editor view) changes will trigger a Dataset query.
+//
+// @property {FacetList} facets: FacetList object containing all current
+// Facets.
 my.Dataset = Backbone.Model.extend({
   __type__: 'Dataset',
+  // ### initialize
+  // 
+  // Sets up instance properties (see above)
   initialize: function(model, backend) {
     _.bindAll(this, 'query');
     this.backend = backend;
@@ -91,9 +107,11 @@ my.Dataset = Backbone.Model.extend({
     }
     this.fields = new my.FieldList();
     this.currentDocuments = new my.DocumentList();
+    this.facets = new my.FacetList();
     this.docCount = null;
     this.queryState = new my.Query();
     this.queryState.bind('change', this.query);
+    this.queryState.bind('facet:add', this.query);
   },
 
   // ### query
@@ -106,18 +124,26 @@ my.Dataset = Backbone.Model.extend({
   // Resulting DocumentList are used to reset this.currentDocuments and are
   // also returned.
   query: function(queryObj) {
-    this.trigger('query:start');
     var self = this;
-    this.queryState.set(queryObj);
+    this.trigger('query:start');
+    var actualQuery = self._prepareQuery(queryObj);
     var dfd = $.Deferred();
-    this.backend.query(this, this.queryState.toJSON()).done(function(rows) {
-      var docs = _.map(rows, function(row) {
-        var _doc = new my.Document(row);
+    this.backend.query(this, actualQuery).done(function(queryResult) {
+      self.docCount = queryResult.total;
+      var docs = _.map(queryResult.hits, function(hit) {
+        var _doc = new my.Document(hit._source);
         _doc.backend = self.backend;
         _doc.dataset = self;
         return _doc;
       });
       self.currentDocuments.reset(docs);
+      if (queryResult.facets) {
+        var facets = _.map(queryResult.facets, function(facetResult, facetId) {
+          facetResult.id = facetId;
+          return new my.Facet(facetResult);
+        });
+        self.facets.reset(facets);
+      }
       self.trigger('query:done');
       dfd.resolve(self.currentDocuments);
     })
@@ -128,6 +154,14 @@ my.Dataset = Backbone.Model.extend({
     return dfd.promise();
   },
 
+  _prepareQuery: function(newQueryObj) {
+    if (newQueryObj) {
+      this.queryState.set(newQueryObj);
+    }
+    var out = this.queryState.toJSON();
+    return out;
+  },
+
   toTemplateJSON: function() {
     var data = this.toJSON();
     data.docCount = this.docCount;
@@ -136,11 +170,29 @@ my.Dataset = Backbone.Model.extend({
   }
 });
 
-// ## A Document (aka Row)
+// ## <a id="document">A Document (aka Row)</a>
 // 
 // A single entry or row in the dataset
 my.Document = Backbone.Model.extend({
-  __type__: 'Document'
+  __type__: 'Document',
+  initialize: function() {
+    _.bindAll(this, 'getFieldValue');
+  },
+
+  // ### getFieldValue
+  //
+  // For the provided Field get the corresponding rendered computed data value
+  // for this document.
+  getFieldValue: function(field) {
+    var val = this.get(field.id);
+    if (field.deriver) {
+      val = field.deriver(val, field, this);
+    }
+    if (field.renderer) {
+      val = field.renderer(val, field, this);
+    }
+    return val;
+  }
 });
 
 // ## A Backbone collection of Documents
@@ -149,29 +201,73 @@ my.DocumentList = Backbone.Collection.extend({
   model: my.Document
 });
 
-// ## A Field (aka Column) on a Dataset
+// ## <a id="field">A Field (aka Column) on a Dataset</a>
 // 
-// Following attributes as standard:
+// Following (Backbone) attributes as standard:
 //
-//  * id: a unique identifer for this field- usually this should match the key in the documents hash
-//  * label: the visible label used for this field
-//  * type: the type of the data
+// * id: a unique identifer for this field- usually this should match the key in the documents hash
+// * label: (optional: defaults to id) the visible label used for this field
+// * type: (optional: defaults to string) the type of the data in this field. Should be a string as per type names defined by ElasticSearch - see Types list on <http://www.elasticsearch.org/guide/reference/mapping/>
+// * format: (optional) used to indicate how the data should be formatted. For example:
+//   * type=date, format=yyyy-mm-dd
+//   * type=float, format=percentage
+//   * type=float, format='###,###.##'
+// * is_derived: (default: false) attribute indicating this field has no backend data but is just derived from other fields (see below).
+// 
+// Following additional instance properties:
+// 
+// @property {Function} renderer: a function to render the data for this field.
+// Signature: function(value, field, doc) where value is the value of this
+// cell, field is corresponding field object and document is the document
+// object. Note that implementing functions can ignore arguments (e.g.
+// function(value) would be a valid formatter function).
+// 
+// @property {Function} deriver: a function to derive/compute the value of data
+// in this field as a function of this field's value (if any) and the current
+// document, its signature and behaviour is the same as for renderer.  Use of
+// this function allows you to define an entirely new value for data in this
+// field. This provides support for a) 'derived/computed' fields: i.e. fields
+// whose data are functions of the data in other fields b) transforming the
+// value of this field prior to rendering.
 my.Field = Backbone.Model.extend({
+  // ### defaults - define default values
   defaults: {
-    id: null,
     label: null,
-    type: 'String'
+    type: 'string',
+    format: null,
+    is_derived: false
   },
-  // In addition to normal backbone initialization via a Hash you can also
-  // just pass a single argument representing id to the ctor
-  initialize: function(data) {
-    // if a hash not passed in the first argument is set as value for key 0
+  // ### initialize
+  //
+  // @param {Object} data: standard Backbone model attributes
+  //
+  // @param {Object} options: renderer and/or deriver functions.
+  initialize: function(data, options) {
+    // if a hash not passed in the first argument throw error
     if ('0' in data) {
       throw new Error('Looks like you did not pass a proper hash with id to Field constructor');
     }
-    if (this.attributes.label == null) {
+    if (this.attributes.label === null) {
       this.set({label: this.id});
     }
+    if (options) {
+      this.renderer = options.renderer;
+      this.deriver = options.deriver;
+    }
+    if (!this.renderer) {
+      this.renderer = this.defaultRenderers[this.get('type')];
+    }
+  },
+  defaultRenderers: {
+    object: function(val, field, doc) {
+      return JSON.stringify(val);
+    },
+    'float': function(val, field, doc) {
+      var format = field.get('format'); 
+      if (format === 'percentage') {
+        return val + '%';
+      }
+    }
   }
 });
 
@@ -179,14 +275,180 @@ my.FieldList = Backbone.Collection.extend({
   model: my.Field
 });
 
-// ## A Query object storing Dataset Query state
+// ## <a id="query">Query</a>
+//
+// Query instances encapsulate a query to the backend (see <a
+// href="backend/base.html">query method on backend</a>). Useful both
+// for creating queries and for storing and manipulating query state -
+// e.g. from a query editor).
+//
+// **Query Structure and format**
+//
+// Query structure should follow that of [ElasticSearch query
+// language](http://www.elasticsearch.org/guide/reference/api/search/).
+//
+// **NB: It is up to specific backends how to implement and support this query
+// structure. Different backends might choose to implement things differently
+// or not support certain features. Please check your backend for details.**
+//
+// Query object has the following key attributes:
+// 
+//  * size (=limit): number of results to return
+//  * from (=offset): offset into result set - http://www.elasticsearch.org/guide/reference/api/search/from-size.html
+//  * sort: sort order - <http://www.elasticsearch.org/guide/reference/api/search/sort.html>
+//  * query: Query in ES Query DSL <http://www.elasticsearch.org/guide/reference/api/search/query.html>
+//  * filter: See filters and <a href="http://www.elasticsearch.org/guide/reference/query-dsl/filtered-query.html">Filtered Query</a>
+//  * fields: set of fields to return - http://www.elasticsearch.org/guide/reference/api/search/fields.html
+//  * facets: specification of facets - see http://www.elasticsearch.org/guide/reference/api/search/facets/
+// 
+// Additions:
+// 
+//  * q: either straight text or a hash will map directly onto a [query_string
+//  query](http://www.elasticsearch.org/guide/reference/query-dsl/query-string-query.html)
+//  in backend
+//
+//   * Of course this can be re-interpreted by different backends. E.g. some
+//   may just pass this straight through e.g. for an SQL backend this could be
+//   the full SQL query
+//
+//  * filters: dict of ElasticSearch filters. These will be and-ed together for
+//  execution.
+// 
+// **Examples**
+// 
+// <pre>
+// {
+//    q: 'quick brown fox',
+//    filters: [
+//      { term: { 'owner': 'jones' } }
+//    ]
+// }
+// </pre>
 my.Query = Backbone.Model.extend({
-  defaults: {
-    size: 100
-    , from: 0
+  defaults: function() {
+    return {
+      size: 100,
+      from: 0,
+      facets: {},
+      // <http://www.elasticsearch.org/guide/reference/query-dsl/and-filter.html>
+      // , filter: {}
+      filters: []
+    };
+  },
+  // #### addTermFilter
+  // 
+  // Set (update or add) a terms filter to filters
+  //
+  // See <http://www.elasticsearch.org/guide/reference/query-dsl/terms-filter.html>
+  addTermFilter: function(fieldId, value) {
+    var filters = this.get('filters');
+    var filter = { term: {} };
+    filter.term[fieldId] = value;
+    filters.push(filter);
+    this.set({filters: filters});
+    // change does not seem to be triggered automatically
+    if (value) {
+      this.trigger('change');
+    } else {
+      // adding a new blank filter and do not want to trigger a new query
+      this.trigger('change:filters:new-blank');
+    }
+  },
+  // ### removeFilter
+  //
+  // Remove a filter from filters at index filterIndex
+  removeFilter: function(filterIndex) {
+    var filters = this.get('filters');
+    filters.splice(filterIndex, 1);
+    this.set({filters: filters});
+    this.trigger('change');
+  },
+  // ### addFacet
+  //
+  // Add a Facet to this query
+  //
+  // See <http://www.elasticsearch.org/guide/reference/api/search/facets/>
+  addFacet: function(fieldId) {
+    var facets = this.get('facets');
+    // Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field)
+    if (_.contains(_.keys(facets), fieldId)) {
+      return;
+    }
+    facets[fieldId] = {
+      terms: { field: fieldId }
+    };
+    this.set({facets: facets}, {silent: true});
+    this.trigger('facet:add', this);
+  },
+  addHistogramFacet: function(fieldId) {
+    var facets = this.get('facets');
+    facets[fieldId] = {
+      date_histogram: {
+        field: fieldId,
+        interval: 'day'
+      }
+    };
+    this.set({facets: facets}, {silent: true});
+    this.trigger('facet:add', this);
+  }
+});
+
+
+// ## <a id="facet">A Facet (Result)</a>
+//
+// Object to store Facet information, that is summary information (e.g. values
+// and counts) about a field obtained by some faceting method on the
+// backend.
+//
+// Structure of a facet follows that of Facet results in ElasticSearch, see:
+// <http://www.elasticsearch.org/guide/reference/api/search/facets/>
+//
+// Specifically the object structure of a facet looks like (there is one
+// addition compared to ElasticSearch: the "id" field which corresponds to the
+// key used to specify this facet in the facet query):
+//
+// <pre>
+// {
+//   "id": "id-of-facet",
+//   // type of this facet (terms, range, histogram etc)
+//   "_type" : "terms",
+//   // total number of tokens in the facet
+//   "total": 5,
+//   // @property {number} number of documents which have no value for the field
+//   "missing" : 0,
+//   // number of facet values not included in the returned facets
+//   "other": 0,
+//   // term object ({term: , count: ...})
+//   "terms" : [ {
+//       "term" : "foo",
+//       "count" : 2
+//     }, {
+//       "term" : "bar",
+//       "count" : 2
+//     }, {
+//       "term" : "baz",
+//       "count" : 1
+//     }
+//   ]
+// }
+// </pre>
+my.Facet = Backbone.Model.extend({
+  defaults: function() {
+    return {
+      _type: 'terms',
+      total: 0,
+      other: 0,
+      missing: 0,
+      terms: []
+    };
   }
 });
 
+// ## A Collection/List of Facets
+my.FacetList = Backbone.Collection.extend({
+  model: my.Facet
+});
+
 // ## Backend registry
 //
 // Backends will register themselves by id into this registry
@@ -194,10 +456,12 @@ my.backends = {};
 
 }(jQuery, this.recline.Model));
 
+/*jshint multistr:true */
+
 var util = function() {
   var templates = {
-    transformActions: '<li><a data-action="transform" class="menuAction" href="JavaScript:void(0);">Global transform...</a></li>'
-    , cellEditor: ' \
+    transformActions: '<li><a data-action="transform" class="menuAction" href="JavaScript:void(0);">Global transform...</a></li>',
+    cellEditor: ' \
       <div class="menu-container data-table-cell-editor"> \
         <textarea class="data-table-cell-editor-editor" bind="textarea">{{value}}</textarea> \
         <div id="data-table-cell-editor-actions"> \
@@ -207,8 +471,8 @@ var util = function() {
           </div> \
         </div> \
       </div> \
-    '
-    , editPreview: ' \
+    ',
+    editPreview: ' \
       <div class="expression-preview-table-wrapper"> \
         <table> \
         <thead> \
@@ -257,7 +521,7 @@ var util = function() {
   function registerEmitter() {
     var Emitter = function(obj) {
       this.emit = function(obj, channel) { 
-        if (!channel) var channel = 'data';
+        if (!channel) channel = 'data';
         this.trigger(channel, obj); 
       };
     };
@@ -274,7 +538,7 @@ var util = function() {
 			104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", 
 			112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", 
 			120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta"
-		}
+		};
     window.addEventListener("keyup", function(e) { 
       var pressed = shortcuts[e.keyCode];
       if(_.include(keys, pressed)) app.emitter.emit("keyup", pressed); 
@@ -320,10 +584,11 @@ var util = function() {
     if ( !options ) options = {data: {}};
     if ( !options.data ) options = {data: options};
     var html = $.mustache( templates[template], options.data );
+    var targetDom = null;
     if (target instanceof jQuery) {
-      var targetDom = target;
+      targetDom = target;
     } else {
-      var targetDom = $( "." + target + ":first" );      
+      targetDom = $( "." + target + ":first" );      
     }
     if( options.append ) {
       targetDom.append( html );
@@ -344,6 +609,8 @@ var util = function() {
     observeExit: observeExit
   };
 }();
+/*jshint multistr:true */
+
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
@@ -424,10 +691,10 @@ my.FlotGraph = Backbone.View.extend({
 ',
 
   events: {
-    'change form select': 'onEditorSubmit'
-    , 'click .editor-add': 'addSeries'
-    , 'click .action-remove-series': 'removeSeries'
-    , 'click .action-toggle-help': 'toggleHelp'
+    'change form select': 'onEditorSubmit',
+    'click .editor-add': 'addSeries',
+    'click .action-remove-series': 'removeSeries',
+    'click .action-toggle-help': 'toggleHelp'
   },
 
   initialize: function(options, config) {
@@ -473,12 +740,12 @@ my.FlotGraph = Backbone.View.extend({
     var series = this.$series.map(function () {
       return $(this).val();
     });
-    this.chartConfig.series = $.makeArray(series)
+    this.chartConfig.series = $.makeArray(series);
     this.chartConfig.group = this.el.find('.editor-group select').val();
     this.chartConfig.graphType = this.el.find('.editor-type select').val();
     // update navigation
     var qs = my.parseHashQueryString();
-    qs['graph'] = JSON.stringify(this.chartConfig);
+    qs.graph = JSON.stringify(this.chartConfig);
     my.setHashQueryString(qs);
     this.redraw();
   },
@@ -491,8 +758,8 @@ my.FlotGraph = Backbone.View.extend({
     //   Uncaught Invalid dimensions for plot, width = 0, height = 0
     // * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value' 
     var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]);
-    if ((!areWeVisible || this.model.currentDocuments.length == 0)) {
-      return
+    if ((!areWeVisible || this.model.currentDocuments.length === 0)) {
+      return;
     }
     var series = this.createSeries();
     var options = this.getGraphOptions(this.chartConfig.graphType);
@@ -525,7 +792,7 @@ my.FlotGraph = Backbone.View.extend({
         }
       }
       return val;
-    }
+    };
     // TODO: we should really use tickFormatter and 1 interval ticks if (and
     // only if) x-axis values are non-numeric
     // However, that is non-trivial to work out from a dataset (datasets may
@@ -535,21 +802,21 @@ my.FlotGraph = Backbone.View.extend({
          series: { 
            lines: { show: true }
          }
-      }
-      , points: {
+      },
+      points: {
         series: {
           points: { show: true }
         },
         grid: { hoverable: true, clickable: true }
-      }
-      , 'lines-and-points': {
+      },
+      'lines-and-points': {
         series: {
           points: { show: true },
           lines: { show: true }
         },
         grid: { hoverable: true, clickable: true }
-      }
-      , bars: {
+      },
+      bars: {
         series: {
           lines: {show: false},
           bars: {
@@ -569,7 +836,7 @@ my.FlotGraph = Backbone.View.extend({
           max: self.model.currentDocuments.length - 0.5
         }
       }
-    }
+    };
     return options[typeId];
   },
 
@@ -696,6 +963,8 @@ my.FlotGraph = Backbone.View.extend({
 
 })(jQuery, recline.View);
 
+/*jshint multistr:true */
+
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
@@ -704,16 +973,12 @@ this.recline.View = this.recline.View || {};
 //
 // Provides a tabular view on a Dataset.
 //
-// Initialize it with a recline.Dataset object.
-//
-// Additional options passed in second arguments. Options:
-//
-// * cellRenderer: function used to render individual cells. See DataGridRow for more.
+// Initialize it with a `recline.Model.Dataset`.
 my.DataGrid = Backbone.View.extend({
   tagName:  "div",
-  className: "data-table-container",
+  className: "recline-grid-container",
 
-  initialize: function(modelEtc, options) {
+  initialize: function(modelEtc) {
     var self = this;
     this.el = $(this.el);
     _.bindAll(this, 'render');
@@ -722,14 +987,13 @@ my.DataGrid = Backbone.View.extend({
     this.model.currentDocuments.bind('remove', this.render);
     this.state = {};
     this.hiddenFields = [];
-    this.options = options;
   },
 
   events: {
-    'click .column-header-menu': 'onColumnHeaderClick'
-    , 'click .row-header-menu': 'onRowHeaderClick'
-    , 'click .root-header-menu': 'onRootHeaderClick'
-    , 'click .data-table-menu li a': 'onMenuClick'
+    'click .column-header-menu': 'onColumnHeaderClick',
+    'click .row-header-menu': 'onRowHeaderClick',
+    'click .root-header-menu': 'onRootHeaderClick',
+    'click .data-table-menu li a': 'onMenuClick'
   },
 
   // TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)).
@@ -768,41 +1032,35 @@ my.DataGrid = Backbone.View.extend({
     var self = this;
     e.preventDefault();
     var actions = {
-      bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}) },
-      transform: function() { self.showTransformDialog('transform') },
-      sortAsc: function() { self.setColumnSort('asc') },
-      sortDesc: function() { self.setColumnSort('desc') },
-      hideColumn: function() { self.hideColumn() },
-      showColumn: function() { self.showColumn(e) },
-      // TODO: Delete or re-implement ...
-      csv: function() { window.location.href = app.csvUrl },
-      json: function() { window.location.href = "_rewrite/api/json" },
-      urlImport: function() { showDialog('urlImport') },
-      pasteImport: function() { showDialog('pasteImport') },
-      uploadImport: function() { showDialog('uploadImport') },
-      // END TODO
-      deleteColumn: function() {
-        var msg = "Are you sure? This will delete '" + self.state.currentColumn + "' from all documents.";
-        // TODO:
-        alert('This function needs to be re-implemented');
-        return;
-        if (confirm(msg)) costco.deleteColumn(self.state.currentColumn);
+      bulkEdit: function() { self.showTransformColumnDialog('bulkEdit', {name: self.state.currentColumn}); },
+      facet: function() { 
+        self.model.queryState.addFacet(self.state.currentColumn);
+      },
+      facet_histogram: function() {
+        self.model.queryState.addHistogramFacet(self.state.currentColumn);
       },
+      filter: function() {
+        self.model.queryState.addTermFilter(self.state.currentColumn, '');
+      },
+      transform: function() { self.showTransformDialog('transform'); },
+      sortAsc: function() { self.setColumnSort('asc'); },
+      sortDesc: function() { self.setColumnSort('desc'); },
+      hideColumn: function() { self.hideColumn(); },
+      showColumn: function() { self.showColumn(e); },
       deleteRow: function() {
         var doc = _.find(self.model.currentDocuments.models, function(doc) {
           // important this is == as the currentRow will be string (as comes
           // from DOM) while id may be int
-          return doc.id == self.state.currentRow
+          return doc.id == self.state.currentRow;
         });
         doc.destroy().then(function() { 
             self.model.currentDocuments.remove(doc);
             my.notify("Row deleted successfully");
-          })
-          .fail(function(err) {
-            my.notify("Errorz! " + err)
-          })
+          }).fail(function(err) {
+            my.notify("Errorz! " + err);
+          });
       }
-    }
+    };
     actions[$(e.target).attr('data-action')]();
   },
 
@@ -818,7 +1076,7 @@ my.DataGrid = Backbone.View.extend({
     $el.append(view.el);
     util.observeExit($el, function() {
       util.hide('dialog');
-    })
+    });
     $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
   },
 
@@ -832,7 +1090,7 @@ my.DataGrid = Backbone.View.extend({
     $el.append(view.el);
     util.observeExit($el, function() {
       util.hide('dialog');
-    })
+    });
     $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' });
   },
 
@@ -855,7 +1113,7 @@ my.DataGrid = Backbone.View.extend({
   // ======================================================
   // #### Templating
   template: ' \
-    <table class="data-table table-striped table-condensed" cellspacing="0"> \
+    <table class="recline-grid table-striped table-condensed" cellspacing="0"> \
       <thead> \
         <tr> \
           {{#notEmpty}} \
@@ -873,11 +1131,16 @@ my.DataGrid = Backbone.View.extend({
               <div class="btn-group column-header-menu"> \
                 <a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \
                 <ul class="dropdown-menu data-table-menu pull-right"> \
-                  <li class="write-op"><a data-action="bulkEdit" href="JavaScript:void(0);">Transform...</a></li> \
-                  <li class="write-op"><a data-action="deleteColumn" href="JavaScript:void(0);">Delete this column</a></li> \
+                  <li><a data-action="facet" href="JavaScript:void(0);">Term Facet</a></li> \
+                  <li><a data-action="facet_histogram" href="JavaScript:void(0);">Date Histogram Facet</a></li> \
+                  <li><a data-action="filter" href="JavaScript:void(0);">Text Filter</a></li> \
+                  <li class="divider"></li> \
                   <li><a data-action="sortAsc" href="JavaScript:void(0);">Sort ascending</a></li> \
                   <li><a data-action="sortDesc" href="JavaScript:void(0);">Sort descending</a></li> \
+                  <li class="divider"></li> \
                   <li><a data-action="hideColumn" href="JavaScript:void(0);">Hide this column</a></li> \
+                  <li class="divider"></li> \
+                  <li class="write-op"><a data-action="bulkEdit" href="JavaScript:void(0);">Transform...</a></li> \
                 </ul> \
               </div> \
               <span class="column-header-name">{{label}}</span> \
@@ -890,10 +1153,10 @@ my.DataGrid = Backbone.View.extend({
   ',
 
   toTemplateJSON: function() {
-    var modelData = this.model.toJSON()
-    modelData.notEmpty = ( this.fields.length > 0 )
+    var modelData = this.model.toJSON();
+    modelData.notEmpty = ( this.fields.length > 0 );
     // TODO: move this sort of thing into a toTemplateJSON method on Dataset?
-    modelData.fields = _.map(this.fields, function(field) { return field.toJSON() });
+    modelData.fields = _.map(this.fields, function(field) { return field.toJSON(); });
     return modelData;
   },
   render: function() {
@@ -910,12 +1173,10 @@ my.DataGrid = Backbone.View.extend({
           model: doc,
           el: tr,
           fields: self.fields
-        },
-        self.options
-        );
+        });
       newView.render();
     });
-    this.el.toggleClass('no-hidden', (self.hiddenFields.length == 0));
+    this.el.toggleClass('no-hidden', (self.hiddenFields.length === 0));
     return this;
   }
 });
@@ -926,14 +1187,6 @@ my.DataGrid = Backbone.View.extend({
 //
 // In addition you *must* pass in a FieldList in the constructor options. This should be list of fields for the DataGrid.
 //
-// Additional options can be passed in a second hash argument. Options:
-//
-// * cellRenderer: function to render cells. Signature: function(value,
-//   field, doc) where value is the value of this cell, field is
-//   corresponding field object and document is the document object. Note
-//   that implementing functions can ignore arguments (e.g.
-//   function(value) would be a valid cellRenderer function).
-//
 // Example:
 //
 // <pre>
@@ -941,22 +1194,12 @@ my.DataGrid = Backbone.View.extend({
 //   model: dataset-document,
 //     el: dom-element,
 //     fields: mydatasets.fields // a FieldList object
-//   }, {
-//     cellRenderer: my-cell-renderer-function 
-//   }
-// );
+//   });
 // </pre>
 my.DataGridRow = Backbone.View.extend({
-  initialize: function(initData, options) {
+  initialize: function(initData) {
     _.bindAll(this, 'render');
     this._fields = initData.fields;
-    if (options && options.cellRenderer) {
-      this._cellRenderer = options.cellRenderer;
-    } else {
-      this._cellRenderer = function(value) {
-        return value;
-      }
-    }
     this.el = $(this.el);
     this.model.bind('change', this.render);
   },
@@ -991,10 +1234,10 @@ my.DataGridRow = Backbone.View.extend({
     var cellData = this._fields.map(function(field) {
       return {
         field: field.id,
-        value: self._cellRenderer(doc.get(field.id), field, doc)
-      }
-    })
-    return { id: this.id, cells: cellData }
+        value: doc.getFieldValue(field)
+      };
+    });
+    return { id: this.id, cells: cellData };
   },
 
   render: function() {
@@ -1044,6 +1287,407 @@ my.DataGridRow = Backbone.View.extend({
 });
 
 })(jQuery, recline.View);
+/*jshint multistr:true */
+
+this.recline = this.recline || {};
+this.recline.View = this.recline.View || {};
+
+(function($, my) {
+
+// ## Map view for a Dataset using Leaflet mapping library.
+//
+// This view allows to plot gereferenced documents on a map. The location
+// information can be provided either via a field with
+// [GeoJSON](http://geojson.org) objects or two fields with latitude and
+// longitude coordinates.
+//
+// Initialization arguments:
+//
+// * options: initial options. They must contain a model:
+//
+//      {
+//          model: {recline.Model.Dataset}
+//      }
+//
+// * config: (optional) map configuration hash (not yet used)
+//
+//
+my.Map = Backbone.View.extend({
+
+  tagName:  'div',
+  className: 'data-map-container',
+
+  template: ' \
+  <div class="editor"> \
+    <form class="form-stacked"> \
+      <div class="clearfix"> \
+        <div class="editor-field-type"> \
+            <label class="radio"> \
+              <input type="radio" id="editor-field-type-latlon" name="editor-field-type" value="latlon" checked="checked"/> \
+              Latitude / Longitude fields</label> \
+            <label class="radio"> \
+              <input type="radio" id="editor-field-type-geom" name="editor-field-type" value="geom" /> \
+              GeoJSON field</label> \
+        </div> \
+        <div class="editor-field-type-latlon"> \
+          <label>Latitude field</label> \
+          <div class="input editor-lat-field"> \
+            <select> \
+            <option value=""></option> \
+            {{#fields}} \
+            <option value="{{id}}">{{label}}</option> \
+            {{/fields}} \
+            </select> \
+          </div> \
+          <label>Longitude field</label> \
+          <div class="input editor-lon-field"> \
+            <select> \
+            <option value=""></option> \
+            {{#fields}} \
+            <option value="{{id}}">{{label}}</option> \
+            {{/fields}} \
+            </select> \
+          </div> \
+        </div> \
+        <div class="editor-field-type-geom" style="display:none"> \
+          <label>Geometry field (GeoJSON)</label> \
+          <div class="input editor-geom-field"> \
+            <select> \
+            <option value=""></option> \
+            {{#fields}} \
+            <option value="{{id}}">{{label}}</option> \
+            {{/fields}} \
+            </select> \
+          </div> \
+        </div> \
+      </div> \
+      <div class="editor-buttons"> \
+        <button class="btn editor-update-map">Update</button> \
+      </div> \
+      <input type="hidden" class="editor-id" value="map-1" /> \
+      </div> \
+    </form> \
+  </div> \
+<div class="panel map"> \
+</div> \
+',
+
+  // These are the default field names that will be used if found.
+  // If not found, the user will need to define the fields via the editor.
+  latitudeFieldNames: ['lat','latitude'],
+  longitudeFieldNames: ['lon','longitude'],
+  geometryFieldNames: ['geom','the_geom','geometry','spatial','location'],
+
+  // Define here events for UI elements
+  events: {
+    'click .editor-update-map': 'onEditorSubmit',
+    'change .editor-field-type': 'onFieldTypeChange'
+  },
+
+
+  initialize: function(options, config) {
+    var self = this;
+
+    this.el = $(this.el);
+
+    // Listen to changes in the fields
+    this.model.bind('change', function() {
+      self._setupGeometryField();
+    });
+    this.model.fields.bind('add', this.render);
+    this.model.fields.bind('reset', function(){
+      self._setupGeometryField()
+      self.render()
+    });
+
+    // Listen to changes in the documents
+    this.model.currentDocuments.bind('add', function(doc){self.redraw('add',doc)});
+    this.model.currentDocuments.bind('remove', function(doc){self.redraw('remove',doc)});
+    this.model.currentDocuments.bind('reset', function(){self.redraw('reset')});
+
+    // If the div was hidden, Leaflet needs to recalculate some sizes
+    // to display properly
+    this.bind('view:show',function(){
+        self.map.invalidateSize();
+    });
+
+    this.mapReady = false;
+
+    this.render();
+  },
+
+  // Public: Adds the necessary elements to the page.
+  //
+  // Also sets up the editor fields and the map if necessary.
+  render: function() {
+
+    var self = this;
+
+    htmls = $.mustache(this.template, this.model.toTemplateJSON());
+
+    $(this.el).html(htmls);
+    this.$map = this.el.find('.panel.map');
+
+    if (this.geomReady && this.model.fields.length){
+      if (this._geomFieldName){
+        this._selectOption('editor-geom-field',this._geomFieldName);
+        $('#editor-field-type-geom').attr('checked','checked').change();
+      } else{
+        this._selectOption('editor-lon-field',this._lonFieldName);
+        this._selectOption('editor-lat-field',this._latFieldName);
+        $('#editor-field-type-latlon').attr('checked','checked').change();
+      }
+    }
+
+    this.model.bind('query:done', function() {
+      if (!self.geomReady){
+        self._setupGeometryField();
+      }
+
+      if (!self.mapReady){
+        self._setupMap();
+      }
+      self.redraw();
+    });
+
+    return this;
+  },
+
+  // Public: Redraws the features on the map according to the action provided
+  //
+  // Actions can be:
+  //
+  // * reset: Clear all features
+  // * add: Add one or n features (documents)
+  // * remove: Remove one or n features (documents)
+  // * refresh: Clear existing features and add all current documents
+  //
+  redraw: function(action,doc){
+
+    var self = this;
+
+    action = action || 'refresh';
+
+    if (this.geomReady && this.mapReady){
+      if (action == 'reset'){
+        this.features.clearLayers();
+      } else if (action == 'add' && doc){
+        this._add(doc);
+      } else if (action == 'remove' && doc){
+        this._remove(doc);
+      } else if (action == 'refresh'){
+        this.features.clearLayers();
+        this._add(this.model.currentDocuments.models);
+      }
+    }
+  },
+
+  //
+  // UI Event handlers
+  //
+
+  // Public: Update map with user options
+  //
+  // Right now the only configurable option is what field(s) contains the
+  // location information.
+  //
+  onEditorSubmit: function(e){
+    e.preventDefault();
+    if ($('#editor-field-type-geom').attr('checked')){
+        this._geomFieldName = $('.editor-geom-field > select > option:selected').val();
+        this._latFieldName = this._lonFieldName = false;
+    } else {
+        this._geomFieldName = false;
+        this._latFieldName = $('.editor-lat-field > select > option:selected').val();
+        this._lonFieldName = $('.editor-lon-field > select > option:selected').val();
+    }
+    this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
+    this.redraw();
+
+    return false;
+  },
+
+  // Public: Shows the relevant select lists depending on the location field
+  // type selected.
+  //
+  onFieldTypeChange: function(e){
+    if (e.target.value == 'geom'){
+        $('.editor-field-type-geom').show();
+        $('.editor-field-type-latlon').hide();
+    } else {
+        $('.editor-field-type-geom').hide();
+        $('.editor-field-type-latlon').show();
+    }
+  },
+
+  // Private: Add one or n features to the map
+  //
+  // For each document passed, a GeoJSON geometry will be extracted and added
+  // to the features layer. If an exception is thrown, the process will be
+  // stopped and an error notification shown.
+  //
+  // Each feature will have a popup associated with all the document fields.
+  //
+  _add: function(docs){
+
+    var self = this;
+
+    if (!(docs instanceof Array)) docs = [docs];
+
+    _.every(docs,function(doc){
+      var feature = self._getGeometryFromDocument(doc);
+      if (typeof feature === 'undefined'){
+        // Empty field
+        return true;
+      } else if (feature instanceof Object){
+        // Build popup contents
+        // TODO: mustache?
+        html = ''
+        for (key in doc.attributes){
+          html += '<div><strong>' + key + '</strong>: '+ doc.attributes[key] + '</div>'
+        }
+        feature.properties = {popupContent: html};
+
+        // Add a reference to the model id, which will allow us to
+        // link this Leaflet layer to a Recline doc
+        feature.properties.cid = doc.cid;
+
+        try {
+            self.features.addGeoJSON(feature);
+        } catch (except) {
+            var msg = 'Wrong geometry value';
+            if (except.message) msg += ' (' + except.message + ')';
+            my.notify(msg,{category:'error'});
+            return false;
+        }
+      } else {
+        my.notify('Wrong geometry value',{category:'error'});
+        return false;
+      }
+      return true;
+    });
+  },
+
+  // Private: Remove one or n features to the map
+  //
+  _remove: function(docs){
+
+    var self = this;
+
+    if (!(docs instanceof Array)) docs = [docs];
+
+    _.each(doc,function(doc){
+      for (key in self.features._layers){
+        if (self.features._layers[key].cid == doc.cid){
+          self.features.removeLayer(self.features._layers[key]);
+        }
+      }
+    });
+
+  },
+
+  // Private: Return a GeoJSON geomtry extracted from the document fields
+  //
+  _getGeometryFromDocument: function(doc){
+    if (this.geomReady){
+      if (this._geomFieldName){
+        // We assume that the contents of the field are a valid GeoJSON object
+        return doc.attributes[this._geomFieldName];
+      } else if (this._lonFieldName && this._latFieldName){
+        // We'll create a GeoJSON like point object from the two lat/lon fields
+        return {
+          type: 'Point',
+          coordinates: [
+            doc.attributes[this._lonFieldName],
+            doc.attributes[this._latFieldName]
+            ]
+        };
+      }
+      return null;
+    }
+  },
+
+  // Private: Check if there is a field with GeoJSON geometries or alternatively,
+  // two fields with lat/lon values.
+  //
+  // If not found, the user can define them via the UI form.
+  _setupGeometryField: function(){
+    var geomField, latField, lonField;
+
+    this._geomFieldName = this._checkField(this.geometryFieldNames);
+    this._latFieldName = this._checkField(this.latitudeFieldNames);
+    this._lonFieldName = this._checkField(this.longitudeFieldNames);
+
+    this.geomReady = (this._geomFieldName || (this._latFieldName && this._lonFieldName));
+  },
+
+  // Private: Check if a field in the current model exists in the provided
+  // list of names.
+  //
+  //
+  _checkField: function(fieldNames){
+    var field;
+    var modelFieldNames = this.model.fields.pluck('id');
+    for (var i = 0; i < fieldNames.length; i++){
+      for (var j = 0; j < modelFieldNames.length; j++){
+        if (modelFieldNames[j].toLowerCase() == fieldNames[i].toLowerCase())
+          return modelFieldNames[j];
+      }
+    }
+    return null;
+  },
+
+  // Private: Sets up the Leaflet map control and the features layer.
+  //
+  // The map uses a base layer from [MapQuest](http://www.mapquest.com) based
+  // on [OpenStreetMap](http://openstreetmap.org).
+  //
+  _setupMap: function(){
+
+    this.map = new L.Map(this.$map.get(0));
+
+    var mapUrl = "http://otile{s}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png";
+    var osmAttribution = 'Map data © 2011 OpenStreetMap contributors, Tiles Courtesy of <a href="http://www.mapquest.com/" target="_blank">MapQuest</a> <img src="http://developer.mapquest.com/content/osm/mq_logo.png">';
+    var bg = new L.TileLayer(mapUrl, {maxZoom: 18, attribution: osmAttribution ,subdomains: '1234'});
+    this.map.addLayer(bg);
+
+    this.features = new L.GeoJSON();
+    this.features.on('featureparse', function (e) {
+      if (e.properties && e.properties.popupContent){
+        e.layer.bindPopup(e.properties.popupContent);
+       }
+      if (e.properties && e.properties.cid){
+        e.layer.cid = e.properties.cid;
+       }
+
+    });
+    this.map.addLayer(this.features);
+
+    this.map.setView(new L.LatLng(0, 0), 2);
+
+    this.mapReady = true;
+  },
+
+  // Private: Helper function to select an option from a select list
+  //
+  _selectOption: function(id,value){
+    var options = $('.' + id + ' > select > option');
+    if (options){
+      options.each(function(opt){
+        if (this.value == value) {
+          $(this).attr('selected','selected');
+          return false;
+        }
+      });
+    }
+  }
+
+ });
+
+})(jQuery, recline.View);
+
+/*jshint multistr:true */
+
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
@@ -1176,8 +1820,8 @@ my.ColumnTransform = Backbone.View.extend({
   ',
 
   events: {
-    'click .okButton': 'onSubmit'
-    , 'keydown .expression-preview-code': 'onEditorKeydown'
+    'click .okButton': 'onSubmit',
+    'keydown .expression-preview-code': 'onEditorKeydown'
   },
 
   initialize: function() {
@@ -1187,7 +1831,7 @@ my.ColumnTransform = Backbone.View.extend({
   render: function() {
     var htmls = $.mustache(this.template, 
       {name: this.state.currentColumn}
-      )
+      );
     this.el.html(htmls);
     // Put in the basic (identity) transform script
     // TODO: put this into the template?
@@ -1225,7 +1869,7 @@ my.ColumnTransform = Backbone.View.extend({
     _.each(toUpdate, function(editedDoc) {
       var realDoc = self.model.currentDocuments.get(editedDoc.id);
       realDoc.set(editedDoc);
-      realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate)
+      realDoc.save().then(onCompletedUpdate).fail(onCompletedUpdate);
     });
   },
 
@@ -1250,6 +1894,7 @@ my.ColumnTransform = Backbone.View.extend({
 });
 
 })(jQuery, recline.View);
+/*jshint multistr:true */
 this.recline = this.recline || {};
 this.recline.View = this.recline.View || {};
 
@@ -1305,7 +1950,7 @@ this.recline.View = this.recline.View || {};
 // FlotGraph subview.
 my.DataExplorer = Backbone.View.extend({
   template: ' \
-  <div class="data-explorer"> \
+  <div class="recline-data-explorer"> \
     <div class="alert-messages"></div> \
     \
     <div class="header"> \
@@ -1317,6 +1962,12 @@ my.DataExplorer = Backbone.View.extend({
       <div class="recline-results-info"> \
         Results found <span class="doc-count">{{docCount}}</span> \
       </div> \
+      <div class="menu-right"> \
+        <a href="#" class="btn" data-action="filters">Filters</a> \
+        <a href="#" class="btn" data-action="facets">Facets</a> \
+      </div> \
+      <div class="query-editor-here" style="display:inline;"></div> \
+      <div class="clearfix"></div> \
     </div> \
     <div class="data-view-container"></div> \
     <div class="dialog-overlay" style="display: none; z-index: 101; "> </div> \
@@ -1327,6 +1978,9 @@ my.DataExplorer = Backbone.View.extend({
     </div> \
   </div> \
   ',
+  events: {
+    'click .menu-right a': 'onMenuClick'
+  },
 
   initialize: function(options) {
     var self = this;
@@ -1365,7 +2019,7 @@ my.DataExplorer = Backbone.View.extend({
         my.notify('Data loaded', {category: 'success'});
         // update navigation
         var qs = my.parseHashQueryString();
-        qs['reclineQuery'] = JSON.stringify(self.model.queryState.toJSON());
+        qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON());
         var out = my.getNewHashForQueryString(qs);
         self.router.navigate(out);
       });
@@ -1414,12 +2068,22 @@ my.DataExplorer = Backbone.View.extend({
     $(this.el).html(template);
     var $dataViewContainer = this.el.find('.data-view-container');
     _.each(this.pageViews, function(view, pageName) {
-      $dataViewContainer.append(view.view.el)
+      $dataViewContainer.append(view.view.el);
     });
     var queryEditor = new my.QueryEditor({
       model: this.model.queryState
     });
-    this.el.find('.header').append(queryEditor.el);
+    this.el.find('.query-editor-here').append(queryEditor.el);
+    var filterEditor = new my.FilterEditor({
+      model: this.model.queryState
+    });
+    this.$filterEditor = filterEditor.el;
+    this.el.find('.header').append(filterEditor.el);
+    var facetViewer = new my.FacetViewer({
+      model: this.model
+    });
+    this.$facetViewer = facetViewer.el;
+    this.el.find('.header').append(facetViewer.el);
   },
 
   setupRouting: function() {
@@ -1445,14 +2109,25 @@ my.DataExplorer = Backbone.View.extend({
     _.each(this.pageViews, function(view, idx) {
       if (view.id === pageName) {
         view.view.el.show();
+        view.view.trigger('view:show');
       } else {
         view.view.el.hide();
+        view.view.trigger('view:hide');
       }
     });
+  },
+
+  onMenuClick: function(e) {
+    e.preventDefault();
+    var action = $(e.target).attr('data-action');
+    if (action === 'filters') {
+      this.$filterEditor.show();
+    } else if (action === 'facets') {
+      this.$facetViewer.show();
+    }
   }
 });
 
-
 my.QueryEditor = Backbone.View.extend({
   className: 'recline-query-editor', 
   template: ' \
@@ -1460,28 +2135,21 @@ my.QueryEditor = Backbone.View.extend({
       <div class="input-prepend text-query"> \
         <span class="add-on"><i class="icon-search"></i></span> \
         <input type="text" name="q" value="{{q}}" class="span2" placeholder="Search data ..." class="search-query" /> \
-        <div class="btn-group menu"> \
-          <a class="btn dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i><span class="caret"></span></a> \
-          <ul class="dropdown-menu"> \
-            <li><a data-action="size" href="">Number of items to show ({{size}})</a></li> \
-            <li><a data-action="from" href="">Show from ({{from}})</a></li> \
-          </ul> \
-        </div> \
       </div> \
       <div class="pagination"> \
         <ul> \
           <li class="prev action-pagination-update"><a href="">«</a></li> \
-          <li class="active"><a>{{from}} – {{to}}</a></li> \
+          <li class="active"><a><input name="from" type="text" value="{{from}}" /> – <input name="to" type="text" value="{{to}}" /> </a></li> \
           <li class="next action-pagination-update"><a href="">»</a></li> \
         </ul> \
       </div> \
+      <button type="submit" class="btn">Go »</button> \
     </form> \
   ',
 
   events: {
-    'submit form': 'onFormSubmit'
-    , 'click .action-pagination-update': 'onPaginationUpdate'
-    , 'click .menu li a': 'onMenuItemClick'
+    'submit form': 'onFormSubmit',
+    'click .action-pagination-update': 'onPaginationUpdate'
   },
 
   initialize: function() {
@@ -1493,32 +2161,21 @@ my.QueryEditor = Backbone.View.extend({
   onFormSubmit: function(e) {
     e.preventDefault();
     var query = this.el.find('.text-query input').val();
-    this.model.set({q: query});
+    var newFrom = parseInt(this.el.find('input[name="from"]').val());
+    var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom;
+    this.model.set({size: newSize, from: newFrom, q: query});
   },
   onPaginationUpdate: function(e) {
     e.preventDefault();
     var $el = $(e.target);
+    var newFrom = 0;
     if ($el.parent().hasClass('prev')) {
-      var newFrom = this.model.get('from') - Math.max(0, this.model.get('size'));
+      newFrom = this.model.get('from') - Math.max(0, this.model.get('size'));
     } else {
-      var newFrom = this.model.get('from') + this.model.get('size');
+      newFrom = this.model.get('from') + this.model.get('size');
     }
     this.model.set({from: newFrom});
   },
-  onMenuItemClick: function(e) {
-    e.preventDefault();
-    var attrName = $(e.target).attr('data-action');
-    var msg = _.template('New value (<%= value %>)',
-        {value: this.model.get(attrName)}
-        );
-    var newValue = prompt(msg);
-    if (newValue) {
-      newValue = parseInt(newValue);
-      var update = {};
-      update[attrName] = newValue;
-      this.model.set(update);
-    }
-  },
   render: function() {
     var tmplData = this.model.toJSON();
     tmplData.to = this.model.get('from') + this.model.get('size');
@@ -1527,6 +2184,174 @@ my.QueryEditor = Backbone.View.extend({
   }
 });
 
+my.FilterEditor = Backbone.View.extend({
+  className: 'recline-filter-editor well', 
+  template: ' \
+    <a class="close js-hide" href="#">×</a> \
+    <div class="row filters"> \
+      <div class="span1"> \
+        <h3>Filters</h3> \
+      </div> \
+      <div class="span11"> \
+        <form class="form-horizontal"> \
+          <div class="row"> \
+            <div class="span6"> \
+              {{#termFilters}} \
+              <div class="control-group filter-term filter" data-filter-id={{id}}> \
+                <label class="control-label" for="">{{label}}</label> \
+                <div class="controls"> \
+                  <div class="input-append"> \
+                    <input type="text" value="{{value}}" name="{{fieldId}}" class="span4" data-filter-field="{{fieldId}}" data-filter-id="{{id}}" data-filter-type="term" /> \
+                    <a class="btn js-remove-filter"><i class="icon-remove"></i></a> \
+                  </div> \
+                </div> \
+              </div> \
+              {{/termFilters}} \
+            </div> \
+          <div class="span4"> \
+            <p>To add a filter use the column menu in the grid view.</p> \
+            <button type="submit" class="btn">Update</button> \
+          </div> \
+        </form> \
+      </div> \
+    </div> \
+  ',
+  events: {
+    'click .js-hide': 'onHide',
+    'click .js-remove-filter': 'onRemoveFilter',
+    'submit form': 'onTermFiltersUpdate'
+  },
+  initialize: function() {
+    this.el = $(this.el);
+    _.bindAll(this, 'render');
+    this.model.bind('change', this.render);
+    this.model.bind('change:filters:new-blank', this.render);
+    this.render();
+  },
+  render: function() {
+    var tmplData = $.extend(true, {}, this.model.toJSON());
+    // we will use idx in list as there id ...
+    tmplData.filters = _.map(tmplData.filters, function(filter, idx) {
+      filter.id = idx;
+      return filter;
+    });
+    tmplData.termFilters = _.filter(tmplData.filters, function(filter) {
+      return filter.term !== undefined;
+    });
+    tmplData.termFilters = _.map(tmplData.termFilters, function(filter) {
+      var fieldId = _.keys(filter.term)[0];
+      return {
+        id: filter.id,
+        fieldId: fieldId,
+        label: fieldId,
+        value: filter.term[fieldId]
+      };
+    });
+    var out = $.mustache(this.template, tmplData);
+    this.el.html(out);
+    // are there actually any facets to show?
+    if (this.model.get('filters').length > 0) {
+      this.el.show();
+    } else {
+      this.el.hide();
+    }
+  },
+  onHide: function(e) {
+    e.preventDefault();
+    this.el.hide();
+  },
+  onRemoveFilter: function(e) {
+    e.preventDefault();
+    var $target = $(e.target);
+    var filterId = $target.closest('.filter').attr('data-filter-id');
+    this.model.removeFilter(filterId);
+  },
+  onTermFiltersUpdate: function(e) {
+   var self = this;
+    e.preventDefault();
+    var filters = self.model.get('filters');
+    var $form = $(e.target);
+    _.each($form.find('input'), function(input) {
+      var $input = $(input);
+      var filterIndex = parseInt($input.attr('data-filter-id'));
+      var value = $input.val();
+      var fieldId = $input.attr('data-filter-field');
+      filters[filterIndex].term[fieldId] = value;
+    });
+    self.model.set({filters: filters});
+    self.model.trigger('change');
+  }
+});
+
+my.FacetViewer = Backbone.View.extend({
+  className: 'recline-facet-viewer well', 
+  template: ' \
+    <a class="close js-hide" href="#">×</a> \
+    <div class="facets row"> \
+      <div class="span1"> \
+        <h3>Facets</h3> \
+      </div> \
+      {{#facets}} \
+      <div class="facet-summary span2 dropdown" data-facet="{{id}}"> \
+        <a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><i class="icon-chevron-down"></i> {{id}} {{label}}</a> \
+        <ul class="facet-items dropdown-menu"> \
+        {{#terms}} \
+          <li><a class="facet-choice js-facet-filter" data-value="{{term}}">{{term}} ({{count}})</a></li> \
+        {{/terms}} \
+        {{#entries}} \
+          <li><a class="facet-choice js-facet-filter" data-value="{{time}}">{{term}} ({{count}})</a></li> \
+        {{/entries}} \
+        </ul> \
+      </div> \
+      {{/facets}} \
+    </div> \
+  ',
+
+  events: {
+    'click .js-hide': 'onHide',
+    'click .js-facet-filter': 'onFacetFilter'
+  },
+  initialize: function(model) {
+    _.bindAll(this, 'render');
+    this.el = $(this.el);
+    this.model.facets.bind('all', this.render);
+    this.model.fields.bind('all', this.render);
+    this.render();
+  },
+  render: function() {
+    var tmplData = {
+      facets: this.model.facets.toJSON(),
+      fields: this.model.fields.toJSON()
+    };
+    tmplData.facets = _.map(tmplData.facets, function(facet) {
+      if (facet._type === 'date_histogram') {
+        facet.entries = _.map(facet.entries, function(entry) {
+          entry.term = new Date(entry.time).toDateString();
+          return entry;
+        });
+      }
+      return facet;
+    });
+    var templated = $.mustache(this.template, tmplData);
+    this.el.html(templated);
+    // are there actually any facets to show?
+    if (this.model.facets.length > 0) {
+      this.el.show();
+    } else {
+      this.el.hide();
+    }
+  },
+  onHide: function(e) {
+    e.preventDefault();
+    this.el.hide();
+  },
+  onFacetFilter: function(e) {
+    var $target= $(e.target);
+    var fieldId = $target.closest('.facet-summary').attr('data-facet');
+    var value = $target.attr('data-value');
+    this.model.queryState.addTermFilter(fieldId, value);
+  }
+});
 
 /* ========================================================== */
 // ## Miscellaneous Utilities
@@ -1536,15 +2361,15 @@ var urlPathRegex = /^([^?]+)(\?.*)?/;
 // Parse the Hash section of a URL into path and query string
 my.parseHashUrl = function(hashUrl) {
   var parsed = urlPathRegex.exec(hashUrl);
-  if (parsed == null) {
+  if (parsed === null) {
     return {};
   } else {
     return {
       path: parsed[1],
       query: parsed[2] || ''
-    }
+    };
   }
-}
+};
 
 // Parse a URL query string (?xyz=abc...) into a dictionary.
 my.parseQueryString = function(q) {
@@ -1565,13 +2390,13 @@ my.parseQueryString = function(q) {
     urlParams[d(e[1])] = d(e[2]);
   }
   return urlParams;
-}
+};
 
 // Parse the query string out of the URL hash
 my.parseHashQueryString = function() {
   q = my.parseHashUrl(window.location.hash).query;
   return my.parseQueryString(q);
-}
+};
 
 // Compse a Query String
 my.composeQueryString = function(queryParams) {
@@ -1582,7 +2407,7 @@ my.composeQueryString = function(queryParams) {
   });
   queryString += items.join('&');
   return queryString;
-}
+};
 
 my.getNewHashForQueryString = function(queryParams) {
   var queryPart = my.composeQueryString(queryParams);
@@ -1592,11 +2417,11 @@ my.getNewHashForQueryString = function(queryParams) {
   } else {
     return queryPart;
   }
-}
+};
 
 my.setHashQueryString = function(queryParams) {
   window.location.hash = my.getNewHashForQueryString(queryParams);
-}
+};
 
 // ## notify
 //
@@ -1606,7 +2431,7 @@ my.setHashQueryString = function(queryParams) {
 // * persist: if true alert is persistent, o/w hidden after 3s (default = false)
 // * loader: if true show loading spinner
 my.notify = function(message, options) {
-  if (!options) var options = {};
+  if (!options) options = {};
   var tmplData = _.extend({
     msg: message,
     category: 'warning'
@@ -1620,7 +2445,7 @@ my.notify = function(message, options) {
         {{/loader}} \
     </div>';
   var _templated = $.mustache(_template, tmplData); 
-  _templated = $(_templated).appendTo($('.data-explorer .alert-messages'));
+  _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages'));
   if (!options.persist) {
     setTimeout(function() {
       $(_templated).fadeOut(1000, function() {
@@ -1628,15 +2453,15 @@ my.notify = function(message, options) {
       });
     }, 1000);
   }
-}
+};
 
 // ## clearNotifications
 //
 // Clear all existing notifications
 my.clearNotifications = function() {
-  var $notifications = $('.data-explorer .alert-messages .alert');
+  var $notifications = $('.recline-data-explorer .alert-messages .alert');
   $notifications.remove();
-}
+};
 
 })(jQuery, recline.View);
 
@@ -1644,7 +2469,7 @@ my.clearNotifications = function() {
 //
 // Backends are connectors to backend data sources and stores
 //
-// This is just the base module containing various convenience methods.
+// This is just the base module containing a template Base class and convenience methods.
 this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 
@@ -1654,32 +2479,113 @@ this.recline.Backend = this.recline.Backend || {};
   // Override Backbone.sync to hand off to sync function in relevant backend
   Backbone.sync = function(method, model, options) {
     return model.backend.sync(method, model, options);
-  }
+  };
 
-  // ## wrapInTimeout
-  // 
-  // Crude way to catch backend errors
-  // Many of backends use JSONP and so will not get error messages and this is
-  // a crude way to catch those errors.
-  my.wrapInTimeout = function(ourFunction) {
-    var dfd = $.Deferred();
-    var timeout = 5000;
-    var timer = setTimeout(function() {
-      dfd.reject({
-        message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds'
+  // ## recline.Backend.Base
+  //
+  // Base class for backends providing a template and convenience functions.
+  // You do not have to inherit from this class but even when not it does provide guidance on the functions you must implement.
+  //
+  // Note also that while this (and other Backends) are implemented as Backbone models this is just a convenience.
+  my.Base = Backbone.Model.extend({
+
+    // ### sync
+    //
+    // An implementation of Backbone.sync that will be used to override
+    // Backbone.sync on operations for Datasets and Documents which are using this backend.
+    //
+    // For read-only implementations you will need only to implement read method
+    // for Dataset models (and even this can be a null operation). The read method
+    // should return relevant metadata for the Dataset. We do not require read support
+    // for Documents because they are loaded in bulk by the query method.
+    //
+    // For backends supporting write operations you must implement update and delete support for Document objects.
+    //
+    // All code paths should return an object conforming to the jquery promise API.
+    sync: function(method, model, options) {
+    },
+    
+    // ### query
+    //
+    // Query the backend for documents returning them in bulk. This method will
+    // be used by the Dataset.query method to search the backend for documents,
+    // retrieving the results in bulk.
+    //
+    // @param {recline.model.Dataset} model: Dataset model.
+    //
+    // @param {Object} queryObj: object describing a query (usually produced by
+    // using recline.Model.Query and calling toJSON on it).
+    //
+    // The structure of data in the Query object or
+    // Hash should follow that defined in <a
+    // href="http://github.com/okfn/recline/issues/34">issue 34</a>.
+    // (Of course, if you are writing your own backend, and hence
+    // have control over the interpretation of the query object, you
+    // can use whatever structure you like).
+    //
+    // @returns {Promise} promise API object. The promise resolve method will
+    // be called on query completion with a QueryResult object.
+    // 
+    // A QueryResult has the following structure (modelled closely on
+    // ElasticSearch - see <a
+    // href="https://github.com/okfn/recline/issues/57">this issue for more
+    // details</a>):
+    //
+    // <pre>
+    // {
+    //   total: // (required) total number of results (can be null)
+    //   hits: [ // (required) one entry for each result document
+    //     {
+    //        _score:   // (optional) match score for document
+    //        _type: // (optional) document type
+    //        _source: // (required) document/row object
+    //     } 
+    //   ],
+    //   facets: { // (optional) 
+    //     // facet results (as per <http://www.elasticsearch.org/guide/reference/api/search/facets/>)
+    //   }
+    // }
+    // </pre>
+    query: function(model, queryObj) {
+    },
+
+    // convenience method to convert simple set of documents / rows to a QueryResult
+    _docsToQueryResult: function(rows) {
+      var hits = _.map(rows, function(row) {
+        return { _source: row };
       });
-    }, timeout);
-    ourFunction.done(function(arguments) {
-        clearTimeout(timer);
-        dfd.resolve(arguments);
-      })
-      .fail(function(arguments) {
-        clearTimeout(timer);
-        dfd.reject(arguments);
-      })
-      ;
-    return dfd.promise();
-  }
+      return {
+        total: null,
+        hits: hits
+      };
+    },
+
+    // ## _wrapInTimeout
+    // 
+    // Convenience method providing a crude way to catch backend errors on JSONP calls.
+    // Many of backends use JSONP and so will not get error messages and this is
+    // a crude way to catch those errors.
+    _wrapInTimeout: function(ourFunction) {
+      var dfd = $.Deferred();
+      var timeout = 5000;
+      var timer = setTimeout(function() {
+        dfd.reject({
+          message: 'Request Error: Backend did not respond after ' + (timeout / 1000) + ' seconds'
+        });
+      }, timeout);
+      ourFunction.done(function(arguments) {
+          clearTimeout(timer);
+          dfd.resolve(arguments);
+        })
+        .fail(function(arguments) {
+          clearTimeout(timer);
+          dfd.reject(arguments);
+        })
+        ;
+      return dfd.promise();
+    }
+  });
+
 }(jQuery, this.recline.Backend));
 
 this.recline = this.recline || {};
@@ -1700,7 +2606,7 @@ this.recline.Backend = this.recline.Backend || {};
   // * format: (optional) csv | xls (defaults to csv if not specified)
   //
   // Note that this is a **read-only** backend.
-  my.DataProxy = Backbone.Model.extend({
+  my.DataProxy = my.Base.extend({
     defaults: {
       dataproxy_url: 'http://jsonpdataproxy.appspot.com'
     },
@@ -1719,19 +2625,20 @@ this.recline.Backend = this.recline.Backend || {};
       }
     },
     query: function(dataset, queryObj) {
+      var self = this;
       var base = this.get('dataproxy_url');
       var data = {
-        url: dataset.get('url')
-        , 'max-results':  queryObj.size
-        , type: dataset.get('format')
+        url: dataset.get('url'),
+        'max-results':  queryObj.size,
+        type: dataset.get('format')
       };
       var jqxhr = $.ajax({
-        url: base
-        , data: data
-        , dataType: 'jsonp'
+        url: base,
+        data: data,
+        dataType: 'jsonp'
       });
       var dfd = $.Deferred();
-      my.wrapInTimeout(jqxhr).done(function(results) {
+      this._wrapInTimeout(jqxhr).done(function(results) {
         if (results.error) {
           dfd.reject(results.error);
         }
@@ -1746,7 +2653,7 @@ this.recline.Backend = this.recline.Backend || {};
           });
           return tmp;
         });
-        dfd.resolve(_out);
+        dfd.resolve(self._docsToQueryResult(_out));
       })
       .fail(function(arguments) {
         dfd.reject(arguments);
@@ -1779,7 +2686,7 @@ this.recline.Backend = this.recline.Backend || {};
   // localhost:9200 with index twitter and type tweet it would be
   //
   // <pre>http://localhost:9200/twitter/tweet</pre>
-  my.ElasticSearch = Backbone.Model.extend({
+  my.ElasticSearch = my.Base.extend({
     _getESUrl: function(dataset) {
       var out = dataset.get('elasticsearch_url');
       if (out) return out;
@@ -1799,7 +2706,7 @@ this.recline.Backend = this.recline.Backend || {};
             dataType: 'jsonp'
           });
           var dfd = $.Deferred();
-          my.wrapInTimeout(jqxhr).done(function(schema) {
+          this._wrapInTimeout(jqxhr).done(function(schema) {
             // only one top level key in ES = the type so we can ignore it
             var key = _.keys(schema)[0];
             var fieldData = _.map(schema[key].properties, function(dict, fieldName) {
@@ -1819,26 +2726,35 @@ this.recline.Backend = this.recline.Backend || {};
       }
     },
     _normalizeQuery: function(queryObj) {
-      if (queryObj.toJSON) {
-        var out = queryObj.toJSON();
-      } else {
-        var out = _.extend({}, queryObj);
-      }
-      if (out.q != undefined && out.q.trim() === '') {
+      var out = queryObj.toJSON ? queryObj.toJSON() : _.extend({}, queryObj);
+      if (out.q !== undefined && out.q.trim() === '') {
         delete out.q;
       }
       if (!out.q) {
         out.query = {
           match_all: {}
-        }
+        };
       } else {
         out.query = {
           query_string: {
             query: out.q
           }
-        }
+        };
         delete out.q;
       }
+      // now do filters (note the *plural*)
+      if (out.filters && out.filters.length) {
+        if (!out.filter) {
+          out.filter = {};
+        }
+        if (!out.filter.and) {
+          out.filter.and = [];
+        }
+        out.filter.and = out.filter.and.concat(out.filters);
+      }
+      if (out.filters !== undefined) {
+        delete out.filters;
+      }
       return out;
     },
     query: function(model, queryObj) {
@@ -1853,13 +2769,15 @@ this.recline.Backend = this.recline.Backend || {};
       var dfd = $.Deferred();
       // TODO: fail case
       jqxhr.done(function(results) {
-        model.docCount = results.hits.total;
-        var docs = _.map(results.hits.hits, function(result) {
-          var _out = result._source;
-          _out.id = result._id;
-          return _out;
+        _.each(results.hits.hits, function(hit) {
+          if (!('id' in hit._source) && hit._id) {
+            hit._source.id = hit._id;
+          }
         });
-        dfd.resolve(docs);
+        if (results.facets) {
+          results.hits.facets = results.facets;
+        }
+        dfd.resolve(results.hits);
       });
       return dfd.promise();
     }
@@ -1886,19 +2804,19 @@ this.recline.Backend = this.recline.Backend || {};
   //   'gdocs'
   // );
   // </pre>
-  my.GDoc = Backbone.Model.extend({
+  my.GDoc = my.Base.extend({
     getUrl: function(dataset) {
       var url = dataset.get('url');
       if (url.indexOf('feeds/list') != -1) {
         return url;
       } else {
         // https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=0
-        var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/
+        var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+).*/;
         var matches = url.match(regex);
         if (matches) {
           var key = matches[1];
           var worksheet = 1;
-          var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json'
+          var out = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json';
           return out;
         } else {
           alert('Failed to extract gdocs key from ' + url);
@@ -1922,8 +2840,9 @@ this.recline.Backend = this.recline.Backend || {};
           // cache data onto dataset (we have loaded whole gdoc it seems!)
           model._dataCache = result.data;
           dfd.resolve(model);
-        })
-        return dfd.promise(); }
+        });
+        return dfd.promise();
+      }
     },
 
     query: function(dataset, queryObj) { 
@@ -1934,10 +2853,12 @@ this.recline.Backend = this.recline.Backend || {};
       // TODO: factor this out as a common method with other backends
       var objs = _.map(dataset._dataCache, function (d) { 
         var obj = {};
-        _.each(_.zip(fields, d), function (x) { obj[x[0]] = x[1]; })
+        _.each(_.zip(fields, d), function (x) {
+          obj[x[0]] = x[1];
+        });
         return obj;
       });
-      dfd.resolve(objs);
+      dfd.resolve(this._docsToQueryResult(objs));
       return dfd;
     },
     gdocsToJavascript:  function(gdocsSpreadsheet) {
@@ -1971,8 +2892,8 @@ this.recline.Backend = this.recline.Backend || {};
         if (gdocsSpreadsheet.feed.entry.length > 0) {
           for (var k in gdocsSpreadsheet.feed.entry[0]) {
             if (k.substr(0, 3) == 'gsx') {
-              var col = k.substr(4)
-                results.field.push(col);
+              var col = k.substr(4);
+              results.field.push(col);
             }
           }
         }
@@ -2009,6 +2930,207 @@ this.recline = this.recline || {};
 this.recline.Backend = this.recline.Backend || {};
 
 (function($, my) {
+  my.loadFromCSVFile = function(file, callback) {
+    var metadata = {
+      id: file.name,
+      file: file
+    };
+    var reader = new FileReader();
+    // TODO
+    reader.onload = function(e) {
+      var dataset = my.csvToDataset(e.target.result);
+      callback(dataset);
+    };
+    reader.onerror = function (e) {
+      alert('Failed to load file. Code: ' + e.target.error.code);
+    };
+    reader.readAsText(file);
+  };
+
+  my.csvToDataset = function(csvString) {
+    var out = my.parseCSV(csvString);
+    fields = _.map(out[0], function(cell) {
+      return { id: cell, label: cell };
+    });
+    var data = _.map(out.slice(1), function(row) {
+      var _doc = {};
+      _.each(out[0], function(fieldId, idx) {
+        _doc[fieldId] = row[idx];
+      });
+      return _doc;
+    });
+    var dataset = recline.Backend.createDataset(data, fields);
+    return dataset;
+  };
+
+	// Converts a Comma Separated Values string into an array of arrays.
+	// Each line in the CSV becomes an array.
+  //
+	// Empty fields are converted to nulls and non-quoted numbers are converted to integers or floats.
+  //
+	// @return The CSV parsed as an array
+	// @type Array
+	// 
+	// @param {String} s The string to convert
+	// @param {Boolean} [trm=false] If set to True leading and trailing whitespace is stripped off of each non-quoted field as it is imported
+  //
+  // Heavily based on uselesscode's JS CSV parser (MIT Licensed):
+  // thttp://www.uselesscode.org/javascript/csv/
+	my.parseCSV= function(s, trm) {
+		// Get rid of any trailing \n
+		s = chomp(s);
+
+		var cur = '', // The character we are currently processing.
+			inQuote = false,
+			fieldQuoted = false,
+			field = '', // Buffer for building up the current field
+			row = [],
+			out = [],
+			i,
+			processField;
+
+		processField = function (field) {
+			if (fieldQuoted !== true) {
+				// If field is empty set to null
+				if (field === '') {
+					field = null;
+				// If the field was not quoted and we are trimming fields, trim it
+				} else if (trm === true) {
+					field = trim(field);
+				}
+
+				// Convert unquoted numbers to their appropriate types
+				if (rxIsInt.test(field)) {
+					field = parseInt(field, 10);
+				} else if (rxIsFloat.test(field)) {
+					field = parseFloat(field, 10);
+				}
+			}
+			return field;
+		};
+
+		for (i = 0; i < s.length; i += 1) {
+			cur = s.charAt(i);
+
+			// If we are at a EOF or EOR
+			if (inQuote === false && (cur === ',' || cur === "\n")) {
+				field = processField(field);
+				// Add the current field to the current row
+				row.push(field);
+				// If this is EOR append row to output and flush row
+				if (cur === "\n") {
+					out.push(row);
+					row = [];
+				}
+				// Flush the field buffer
+				field = '';
+				fieldQuoted = false;
+			} else {
+				// If it's not a ", add it to the field buffer
+				if (cur !== '"') {
+					field += cur;
+				} else {
+					if (!inQuote) {
+						// We are not in a quote, start a quote
+						inQuote = true;
+						fieldQuoted = true;
+					} else {
+						// Next char is ", this is an escaped "
+						if (s.charAt(i + 1) === '"') {
+							field += '"';
+							// Skip the next char
+							i += 1;
+						} else {
+							// It's not escaping, so end quote
+							inQuote = false;
+						}
+					}
+				}
+			}
+		}
+
+		// Add the last field
+		field = processField(field);
+		row.push(field);
+		out.push(row);
+
+		return out;
+	};
+
+	var rxIsInt = /^\d+$/,
+		rxIsFloat = /^\d*\.\d+$|^\d+\.\d*$/,
+		// If a string has leading or trailing space,
+		// contains a comma double quote or a newline
+		// it needs to be quoted in CSV output
+		rxNeedsQuoting = /^\s|\s$|,|"|\n/,
+		trim = (function () {
+			// Fx 3.1 has a native trim function, it's about 10x faster, use it if it exists
+			if (String.prototype.trim) {
+				return function (s) {
+					return s.trim();
+				};
+			} else {
+				return function (s) {
+					return s.replace(/^\s*/, '').replace(/\s*$/, '');
+				};
+			}
+		}());
+
+	function chomp(s) {
+		if (s.charAt(s.length - 1) !== "\n") {
+			// Does not end with \n, just return string
+			return s;
+		} else {
+			// Remove the \n
+			return s.substring(0, s.length - 1);
+		}
+	}
+
+
+}(jQuery, this.recline.Backend));
+this.recline = this.recline || {};
+this.recline.Backend = this.recline.Backend || {};
+
+(function($, my) {
+  // ## createDataset
+  //
+  // Convenience function to create a simple 'in-memory' dataset in one step.
+  //
+  // @param data: list of hashes for each document/row in the data ({key:
+  // value, key: value})
+  // @param fields: (optional) list of field hashes (each hash defining a hash
+  // as per recline.Model.Field). If fields not specified they will be taken
+  // from the data.
+  // @param metadata: (optional) dataset metadata - see recline.Model.Dataset.
+  // If not defined (or id not provided) id will be autogenerated.
+  my.createDataset = function(data, fields, metadata) {
+    if (!metadata) {
+      metadata = {};
+    }
+    if (!metadata.id) {
+      metadata.id = String(Math.floor(Math.random() * 100000000) + 1);
+    }
+    var backend = recline.Model.backends['memory'];
+    var datasetInfo = {
+      documents: data,
+      metadata: metadata
+    };
+    if (fields) {
+      datasetInfo.fields = fields;
+    } else {
+      if (data) {
+        datasetInfo.fields = _.map(data[0], function(value, key) {
+          return {id: key};
+        });
+      }
+    }
+    backend.addDataset(datasetInfo);
+    var dataset = new recline.Model.Dataset({id: metadata.id}, 'memory');
+    dataset.fetch();
+    return dataset;
+  };
+
+
   // ## Memory Backend - uses in-memory data
   //
   // To use it you should provide in your constructor data:
@@ -2037,7 +3159,7 @@ this.recline.Backend = this.recline.Backend || {};
   //  dataset.fetch();
   //  etc ...
   //  </pre>
-  my.Memory = Backbone.Model.extend({
+  my.Memory = my.Base.extend({
     initialize: function() {
       this.datasets = {};
     },
@@ -2046,8 +3168,8 @@ this.recline.Backend = this.recline.Backend || {};
     },
     sync: function(method, model, options) {
       var self = this;
+      var dfd = $.Deferred();
       if (method === "read") {
-        var dfd = $.Deferred();
         if (model.__type__ == 'Dataset') {
           var rawDataset = this.datasets[model.id];
           model.set(rawDataset.metadata);
@@ -2057,7 +3179,6 @@ this.recline.Backend = this.recline.Backend || {};
         }
         return dfd.promise();
       } else if (method === 'update') {
-        var dfd = $.Deferred();
         if (model.__type__ == 'Document') {
           _.each(self.datasets[model.dataset.id].documents, function(doc, idx) {
             if(doc.id === model.id) {
@@ -2068,7 +3189,6 @@ this.recline.Backend = this.recline.Backend || {};
         }
         return dfd.promise();
       } else if (method === 'delete') {
-        var dfd = $.Deferred();
         if (model.__type__ == 'Document') {
           var rawDataset = self.datasets[model.dataset.id];
           var newdocs = _.reject(rawDataset.documents, function(doc) {
@@ -2083,10 +3203,17 @@ this.recline.Backend = this.recline.Backend || {};
       }
     },
     query: function(model, queryObj) {
+      var dfd = $.Deferred();
+      var out = {};
       var numRows = queryObj.size;
       var start = queryObj.from;
-      var dfd = $.Deferred();
       results = this.datasets[model.id].documents;
+      _.each(queryObj.filters, function(filter) {
+        results = _.filter(results, function(doc) {
+          var fieldId = _.keys(filter.term)[0];
+          return (doc[fieldId] == filter.term[fieldId]);
+        });
+      });
       // not complete sorting!
       _.each(queryObj.sort, function(sortObj) {
         var fieldName = _.keys(sortObj)[0];
@@ -2095,9 +3222,49 @@ this.recline.Backend = this.recline.Backend || {};
           return (sortObj[fieldName].order == 'asc') ? _out : -1*_out;
         });
       });
-      var results = results.slice(start, start+numRows);
-      dfd.resolve(results);
+      out.facets = this._computeFacets(results, queryObj);
+      var total = results.length;
+      resultsObj = this._docsToQueryResult(results.slice(start, start+numRows));
+      _.extend(out, resultsObj);
+      out.total = total;
+      dfd.resolve(out);
       return dfd.promise();
+    },
+
+    _computeFacets: function(documents, queryObj) {
+      var facetResults = {};
+      if (!queryObj.facets) {
+        return facetsResults;
+      }
+      _.each(queryObj.facets, function(query, facetId) {
+        facetResults[facetId] = new recline.Model.Facet({id: facetId}).toJSON();
+        facetResults[facetId].termsall = {};
+      });
+      // faceting
+      _.each(documents, function(doc) {
+        _.each(queryObj.facets, function(query, facetId) {
+          var fieldId = query.terms.field;
+          var val = doc[fieldId];
+          var tmp = facetResults[facetId];
+          if (val) {
+            tmp.termsall[val] = tmp.termsall[val] ? tmp.termsall[val] + 1 : 1;
+          } else {
+            tmp.missing = tmp.missing + 1;
+          }
+        });
+      });
+      _.each(queryObj.facets, function(query, facetId) {
+        var tmp = facetResults[facetId];
+        var terms = _.map(tmp.termsall, function(count, term) {
+          return { term: term, count: count };
+        });
+        tmp.terms = _.sortBy(terms, function(item) {
+          // want descending order
+          return -item.count;
+        });
+        tmp.terms = tmp.terms.slice(0, 10);
+      });
+      return facetResults;
     }
   });
   recline.Model.backends['memory'] = new my.Memory();
diff --git a/ckan/templates/_snippet/data-api-help.html b/ckan/templates/_snippet/data-api-help.html
index e165570..5a499c9 100644
--- a/ckan/templates/_snippet/data-api-help.html
+++ b/ckan/templates/_snippet/data-api-help.html
@@ -36,16 +36,6 @@
           <thead></thead>
           <tbody>
             <tr>
-              <th>Base</th>
-              <td><code>${datastore_api}</code></td>
-            </tr>
-            <tr>
-              <th>Query</th>
-              <td> 
-                <code>${datastore_api}/_search</code>
-              </td>
-            </tr>
-            <tr>
               <th>Query example</th>
               <td>
                 <code><a href="${datastore_api}/_search?size=5&pretty=true" target="_blank">${datastore_api}/_search?size=5&pretty=true</a></code>
@@ -57,6 +47,10 @@
                 <code><a href="${datastore_api}/_mapping" target="_blank">${datastore_api}/_mapping?pretty=true</a></code>
               </td>
             </tr>
+            <tr>
+              <th>Base</th>
+              <td><code>${datastore_api}</code></td>
+            </tr>
           </tbody>
         </table>
       </div>
diff --git a/ckan/templates/_util.html b/ckan/templates/_util.html
index c4aa5e1..f470b07 100644
--- a/ckan/templates/_util.html
+++ b/ckan/templates/_util.html
@@ -63,97 +63,13 @@
 
   <!--! List of datasets: pass in a collection of tags and this renders the
         standard dataset listing -->
-  <ul py:def="package_list(packages)" class="datasets">
-    <li py:for="package in packages"
-        class="${'fullyopen' if (package.isopen() and package.resources) else None}">
-      <div class="header">
-        <span class="title">
-          ${h.link_to(package.title or package.name, h.url_for(controller='package', action='read', id=package.name))}
-        </span>
-
-        <div class="search_meta">
-          <py:if test="package.resources">
-            <ul class="dataset_formats">
-              <py:for each="resource in package.resources">
-                <py:if test="resource.format and not resource.format == ''">
-                  <li><a href="${resource.url}"
-                    title="${resource.description}">${resource.format}</a></li>
-                </py:if>
-              </py:for>
-            </ul>
-          </py:if>
-          <ul class="openness">
-            <py:if test="package.isopen()">
-              <li>
-                <a href="http://opendefinition.org/okd/" title="This dataset satisfies the Open Definition.">
-                    <img src="http://assets.okfn.org/images/ok_buttons/od_80x15_blue.png" alt="[Open Data]" />
-                </a>
-              </li>
-            </py:if>
-            <py:if test="not package.isopen()">
-              <li>
-                <span class="closed">
-                  ${h.icon('lock')} Not Openly Licensed
-                </span>
-              </li>
-            </py:if>
-          </ul>
-        </div>
-      </div>
-      <div class="extract">
-        ${h.markdown_extract(package.notes)}
-      </div>
-          <!--ul py:if="package.tags" class="tags">
-            <li py:for="tag in package.tags">${tag.name}</li>
-          </ul-->
-    </li>
-  </ul>
+  <py:def function="package_list(packages)">
+	  ${package_list_from_dict(h.convert_to_dict('package', packages))}
+  </py:def>
 
-  <ul py:def="package_list_from_dict(packages)" class="datasets">
-    <li py:for="package in packages"
-        class="${'fullyopen' if (package.isopen and package.get('resources')) else None}">
-        <div class="header">
-      <span class="title">
-        ${h.link_to(package.get('title') or package.get('name'), h.url_for(controller='package', action='read', id=package.get('name')))}
-      </span>
-
-      <div class="search_meta">
-        <py:if test="package.resources">
-          <ul class="dataset_formats">
-            <py:for each="resource in package.resources">
-              <py:if test="resource.get('format')">
-                <li><a href="${resource.get('url')}"
-                  title="${resource.get('description')}">${resource.get('format')}</a></li>
-              </py:if>
-            </py:for>
-          </ul>
-        </py:if>
-        <ul class="openness">
-          <py:if test="package.isopen">
-            <li>
-              <a href="http://opendefinition.org/okd/" title="This dataset satisfies the Open Definition.">
-                  <img src="http://assets.okfn.org/images/ok_buttons/od_80x15_blue.png" alt="[Open Data]" />
-              </a>
-            </li>
-          </py:if>
-          <py:if test="not package.isopen">
-            <li>
-              <span class="closed">
-                ${h.icon('lock')} Not Openly Licensed
-              </span>
-            </li>
-          </py:if>
-        </ul>
-      </div>
-		</div>
-		<div class="extract">
-			${h.markdown_extract(package.notes)}
-		</div>
-        <!--ul py:if="package.tags" class="tags">
-          <li py:for="tag in package.tags">${tag.name}</li>
-        </ul-->
-    </li>
-  </ul>
+  <py:def function="package_list_from_dict(packages)">
+	${h.snippet('snippets/package_list.html', packages=packages)}
+  </py:def>
 
   <!--! List of dataset groups: pass in a collection of dataset groups
         and this renders the standard group listing -->
@@ -360,107 +276,14 @@
   </table>
 
 
-  <table class="table table-bordered table-striped table-condensed" py:def="revision_list(revisions, allow_compare=False)">
-    <tr>
-      <th>Revision</th><th>Timestamp</th><th>Author</th><th>Entity</th><th>Log Message</th>
-    </tr>
-    <tr
-      class="state-${revision.state}"
-      py:for="revision in revisions"
-      >
-      <td>
-        ${
-          h.link_to(revision.id,
-            h.url_for(
-              controller='revision',
-              action='read',
-              id=revision.id)
-            )
-        }
-        <py:if test="c.revision_change_state_allowed">
-        <div class="actions">
-          <form
-            method="POST"
-            action="${h.url_for(controller='revision',
-                action='edit',
-                id=revision.id)}"
-            id="undelete-${revision.id}"
-            >
-            <py:if test="revision.state!='deleted'">
-            <input type="hidden" name="action" value="delete"/>
-            <input type="submit" name="submit" value="Delete" class="btn btn-small" />
-            </py:if>
-            <py:if test="revision.state=='deleted'">
-            <input type="hidden" name="action" value="undelete"/>
-            <input type="submit" name="submit" value="Undelete" class="btn btn-small" />
-            </py:if>
-          </form>
-        </div>
-        </py:if>
-      </td>
-      <td>${h.render_datetime(revision.timestamp, with_hours=True)}</td>
-      <td>${h.linked_user(revision.author)}</td>
-      <td>
-        <py:for each="pkg in revision.packages">
-          <a href="${h.url_for(controller='package', action='read', id=pkg.name)}">${pkg.name}</a>
-        </py:for>
-        <py:for each="grp in revision.groups">
-          <a href="${h.url_for(controller='group', action='read', id=grp.name)}">${grp.name}</a>
-        </py:for>
-      </td>
-      <td>${revision.message}</td>
-    </tr>
-  </table>
+  <py:def function="revision_list(revisions, allow_compare=False)">
+	${revision_list_from_dict(h.convert_to_dict('revisions', revisions), allow_compare=allow_compare)}
+  </py:def>
 
+  <py:def function="revision_list_from_dict(revisions, allow_compare=False)">
+    ${h.snippet('snippets/revision_list.html', revisions=revisions, allow_compare=allow_compare)}
+  </py:def>
 
-  <table class="table table-bordered table-striped table-condensed" py:def="revision_list_from_dict(revisions, allow_compare=False)">
-    <tr>
-      <th>Revision</th><th>Timestamp</th><th>Author</th><th>Entity</th><th>Log Message</th>
-    </tr>
-    <tr
-      class="state-${revision['state']}"
-      py:for="revision in revisions"
-      >
-      <td>
-        ${
-          h.link_to(revision['id'],
-            h.url_for(
-              controller='revision',
-              action='read',
-              id=revision['id'])
-            )
-        }
-        <py:if test="c.revision_change_state_allowed">
-        <div class="actions">
-          <form
-            method="POST"
-            action="${h.url_for(controller='revision',
-                action='edit',
-                id=revision['id'])}"
-            >
-            <py:if test="revision['state']!='deleted'">
-            <button type="submit" name="action" value="delete" class="btn btn-small">Delete</button>
-            </py:if>
-            <py:if test="revision['state']=='deleted'">
-            <button type="submit" name="action" value="undelete" class="btn btn-small">Undelete</button>
-            </py:if>
-          </form>
-        </div>
-        </py:if>
-      </td>
-      <td>${revision['timestamp']}</td>
-      <td>${h.linked_user(revision['author'])}</td>
-      <td>
-        <py:for each="pkg in revision['packages']">
-          <a href="${h.url_for(controller='package', action='read', id=pkg)}">${pkg}</a>
-        </py:for>
-        <py:for each="grp in revision['groups']">
-          <a href="${h.url_for(controller='group', action='read', id=grp)}">${grp}</a>
-        </py:for>
-      </td>
-      <td>${revision['message']}</td>
-    </tr>
-  </table>
 
   <!--! jsConditionalForIe(ieVersion, tagContent, matchOperator): takes a
         IE version number, a tag or other HTML code that will be wrapped inside
diff --git a/ckan/templates/js_strings.html b/ckan/templates/js_strings.html
index b6ca608..5ba337c 100644
--- a/ckan/templates/js_strings.html
+++ b/ckan/templates/js_strings.html
@@ -29,6 +29,14 @@
   CKAN.Strings.failedToGetCredentialsForUpload = "${_('Failed to get credentials for storage upload. Upload cannot proceed')}";
   CKAN.Strings.checkingUploadPermissions = "${_('Checking upload permissions ...')}";
   CKAN.Strings.uploadingFile = "${_('Uploading file ...')}";
+  CKAN.Strings.dataFile = "${_('Data File')}";
+  CKAN.Strings.api = "${_('API')}";
+  CKAN.Strings.visualization = "${_('Visualization')}";
+  CKAN.Strings.image = "${_('Image')}";
+  CKAN.Strings.metadata = "${_('Metadata')}";
+  CKAN.Strings.documentation = "${_('Documentation')}";
+  CKAN.Strings.code = "${_('Code')}";
+  CKAN.Strings.example = "${_('Example')}";
 
   /*
    * Used in templates.js.
@@ -56,6 +64,9 @@
   CKAN.Strings.extraFields = "${_('Extra Fields')}";
   CKAN.Strings.addExtraField = "${_('Add Extra Field')}";
   CKAN.Strings.deleteResource = "${_('Delete Resource')}";
-
+  CKAN.Strings.youCanUseMarkdown = "${_('You can use %aMarkdown formatting%b here.')}";
+  CKAN.Strings.shouldADataStoreBeEnabled = "${_('Should a %aDataStore table and Data API%b be enabled for this resource?')}";
+  CKAN.Strings.datesAreInISO = "${_('Dates are in %aISO Format%b — eg. %c2012-12-25%d or %c2010-05-31T14:30%d.')}";
+  CKAN.Strings.dataFileUploaded = "${_('Data File (Uploaded)')}";
 </script>
 
diff --git a/ckan/templates/package/history.html b/ckan/templates/package/history.html
index 1ac3a30..b9d034b 100644
--- a/ckan/templates/package/history.html
+++ b/ckan/templates/package/history.html
@@ -34,7 +34,7 @@ <h3 py:if="c.error" class="form-errors">
               <a href="${h.url_for(controller='revision',action='read',id=rev['id'])}">${rev['id'][:4]}…</a>
             </td>
             <td>
-              <a href="${h.url_for(controller='package',action='read',id='%s@%s' % (c.pkg_dict['name'], rev['timestamp']))}" title="${'Read dataset as of %s' % rev['timestamp']}">${h.render_datetime(rev['timestamp'], with_hours=True)}</a></td>
+              <a href="${h.url_for(controller='package',action='read',id='%s@%s' % (c.pkg_dict['name'], rev['timestamp']))}" title="${_('Read dataset as of %s') % rev['timestamp']}">${h.render_datetime(rev['timestamp'], with_hours=True)}</a></td>
             <td>${h.linked_user(rev['author'])}</td>
             <td>${rev['message']}</td>
           </tr> 
diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html
index b1ed8ea..b6c30e6 100644
--- a/ckan/templates/package/resource_read.html
+++ b/ckan/templates/package/resource_read.html
@@ -15,8 +15,13 @@
 
   <py:def function="optional_head">
     <!-- data preview -->
+    <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.css')}" />
+    <!--[if lte IE 8]>
+    <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.ie.css')}" />
+    <![endif]-->
     <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/data-explorer.css')}" />
     <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/graph-flot.css')}" />
+    <link rel="stylesheet" href="${h.url_for_static('/scripts/vendor/recline/css/map.css')}" />
     <style type="text/css">
       .recline-query-editor form, .recline-query-editor .text-query {
         height: 28px;
@@ -26,6 +31,16 @@
         margin: 0;
         padding: 0;
       }
+
+      /* needed for Chrome but not FF */
+      .header .recline-query-editor .add-on {
+        margin-left: -27px;
+      }
+
+      /* needed for FF but not chrome */
+      .header .recline-query-editor .input-prepend {
+        vertical-align: top;
+      }
     </style>
     <!-- /data preview -->
     <style type="text/css">
@@ -168,6 +183,8 @@
     <!-- data preview -->
     <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/jquery.mustache/jquery.mustache.js')}"></script>
     <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/flot/0.7/jquery.flot.js')}"></script>
+    <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/flot/0.7/jquery.flot.js')}"></script>
+    <script type="text/javascript" src="${h.url_for_static('/scripts/vendor/leaflet/0.3.1/leaflet.js')}"></script>
     <script src="${h.url_for_static('/scripts/vendor/recline/recline.js')}"></script>
   </py:def>
 
diff --git a/ckan/templates/snippets/package_list.html b/ckan/templates/snippets/package_list.html
new file mode 100644
index 0000000..c90a4b7
--- /dev/null
+++ b/ckan/templates/snippets/package_list.html
@@ -0,0 +1,55 @@
+<html
+  xmlns="http://www.w3.org/1999/xhtml"
+  xmlns:i18n="http://genshi.edgewall.org/i18n"
+  xmlns:py="http://genshi.edgewall.org/"
+  xmlns:xi="http://www.w3.org/2001/XInclude"
+  py:strip=""
+  >
+
+  <ul class="datasets">
+    <li py:for="package in packages"
+        class="${'fullyopen' if (package.isopen and package.get('resources')) else None}">
+        <div class="header">
+      <span class="title">
+        ${h.link_to(package.get('title') or package.get('name'), h.url_for(controller='package', action='read', id=package.get('name')))}
+      </span>
+
+      <div class="search_meta">
+        <py:if test="package.resources">
+          <ul class="dataset_formats">
+            <py:for each="resource in package.resources">
+              <py:if test="resource.get('format')">
+                <li><a href="${resource.get('url')}" class="resource-url-analytics"
+                  title="${resource.get('description')}">${resource.get('format')}</a></li>
+              </py:if>
+            </py:for>
+          </ul>
+        </py:if>
+        <ul class="openness">
+          <py:if test="package.isopen">
+            <li>
+              <a href="http://opendefinition.org/okd/" title="This dataset satisfies the Open Definition.">
+                  <img src="http://assets.okfn.org/images/ok_buttons/od_80x15_blue.png" alt="[Open Data]" />
+              </a>
+            </li>
+          </py:if>
+          <py:if test="not package.isopen">
+            <li>
+              <span class="closed">
+                ${h.icon('lock')} Not Openly Licensed
+              </span>
+            </li>
+          </py:if>
+        </ul>
+      </div>
+		</div>
+		<div class="extract">
+			${h.markdown_extract(package.notes)}
+		</div>
+        <!--ul py:if="package.tags" class="tags">
+          <li py:for="tag in package.tags">${tag.name}</li>
+        </ul-->
+    </li>
+  </ul>
+
+</html>
diff --git a/ckan/templates/snippets/revision_list.html b/ckan/templates/snippets/revision_list.html
new file mode 100644
index 0000000..1450c93
--- /dev/null
+++ b/ckan/templates/snippets/revision_list.html
@@ -0,0 +1,58 @@
+<html
+  xmlns="http://www.w3.org/1999/xhtml"
+  xmlns:i18n="http://genshi.edgewall.org/i18n"
+  xmlns:py="http://genshi.edgewall.org/"
+  xmlns:xi="http://www.w3.org/2001/XInclude"
+  py:strip=""
+  >
+
+  <table class="table table-bordered table-striped table-condensed">
+    <tr>
+      <th>Revision</th><th>Timestamp</th><th>Author</th><th>Entity</th><th>Log Message</th>
+    </tr>
+    <tr
+      class="state-${revision['state']}"
+      py:for="revision in revisions"
+      >
+      <td>
+        ${
+          h.link_to(revision['id'],
+            h.url_for(
+              controller='revision',
+              action='read',
+              id=revision['id'])
+            )
+        }
+        <py:if test="c.revision_change_state_allowed">
+        <div class="actions">
+          <form
+            method="POST"
+            action="${h.url_for(controller='revision',
+                action='edit',
+                id=revision['id'])}"
+		    id="undelete-${revision.id}"
+            >
+            <py:if test="revision['state']!='deleted'">
+            <button type="submit" name="action" value="delete" class="btn btn-small">Delete</button>
+            </py:if>
+            <py:if test="revision['state']=='deleted'">
+            <button type="submit" name="action" value="undelete" class="btn btn-small">Undelete</button>
+            </py:if>
+          </form>
+        </div>
+        </py:if>
+      </td>
+      <td>${h.render_datetime(revision['timestamp'], with_hours=True)}</td>
+      <td>${h.linked_user(revision['author'])}</td>
+      <td>
+        <py:for each="pkg in revision['packages']">
+          <a href="${h.url_for(controller='package', action='read', id=pkg)}">${pkg}</a>
+        </py:for>
+        <py:for each="grp in revision['groups']">
+          <a href="${h.url_for(controller='group', action='read', id=grp)}">${grp}</a>
+        </py:for>
+      </td>
+      <td>${revision['message']}</td>
+    </tr>
+  </table>
+</html>
diff --git a/ckan/tests/functional/test_admin.py b/ckan/tests/functional/test_admin.py
index e15772f..416f1b0 100644
--- a/ckan/tests/functional/test_admin.py
+++ b/ckan/tests/functional/test_admin.py
@@ -334,7 +334,7 @@ def test_undelete(self):
         url = url_for('ckanadmin', action='trash')
         res = self.app.get(url, extra_environ=as_testsysadmin)
         form = res.forms['undelete-'+rev.id]
-        res = form.submit('submit', status=[302], extra_environ=as_testsysadmin)
+        res = form.submit('action', status=[302], extra_environ=as_testsysadmin)
         res = res.follow(extra_environ=as_testsysadmin)
 
         assert 'Revision updated' in res
diff --git a/ckan/tests/functional/test_storage.py b/ckan/tests/functional/test_storage.py
index 27182b5..ca072e1 100644
--- a/ckan/tests/functional/test_storage.py
+++ b/ckan/tests/functional/test_storage.py
@@ -63,7 +63,7 @@ def teardown_class(cls):
     def test_auth_form(self):
         url = url_for('storage_api_auth_form', label='abc')
         res = self.app.get(url, extra_environ=self.extra_environ, status=200)
-        assert res.json['action'] == u'http://localhost/storage/upload_handle', res.json
+        assert res.json['action'] == u'/storage/upload_handle', res.json
         assert res.json['fields'][-1]['value'] == 'abc', res
 
         url = url_for('storage_api_auth_form', label='abc/xxx')
diff --git a/doc/geospatial.rst b/doc/geospatial.rst
new file mode 100644
index 0000000..d14a55e
--- /dev/null
+++ b/doc/geospatial.rst
@@ -0,0 +1,146 @@
+=======================
+Geospatial Capabilities
+=======================
+
+This page documents the Geospatial features available in CKAN and how to add
+geographic information to your datasets.
+
+Metadata Conventions
+====================
+
+Over time some conventions have emerged regarding storing geospatial information on datasets:
+ 
+* spatial-text: Textual representation of the extent / location of the package
+* spatial: [http://geojson.org GeoJSON_] representation of the extent of the package (Polygon or Point)
+* spatial-uri: Linked Data URI representing the place name
+
+For example:
+
+* spatial-text: United Kingdom 
+* spatial: { "type": "Polygon",  "coordinates": [ [ [0.50, 49.74],[0.5, 59.25], [-6.88, 59.25], [-6.88, 49.74], [0.50, 49.74] ] ] } 
+* spatial-uri: http://www.geonames.org/2635167  
+
+or:
+
+* spatial-text: Matsushima
+* spatial: { "type": "Point",  "coordinates": [ 38.36, 141.07] }
+* spatial-uri: http://www.geonames.org/2111964
+
+Use of these conventions when storing information in CKAN means that your
+material will easily integrate with any extensions or functionality built into
+CKAN, like for instance the automatic geo-indexing of your package (see below).
+
+Geo-enabling your datasets
+==========================
+
+To be able to use the geospatial capabilities of CKAN, you need to enable the
+*spatial_query* plugin of the `Geospatial Extension`_ (Check the README for
+requirements and installation).
+
+.. _Geospatial Extension: http://github.com/okfn/ckanext-spatial
+
+This extension adds support for geographic extents for datasets, creating a
+package_extent table that stores the provided in a geometry type column
+(PostGIS_ is used as the backend and GeoAlchemy_ as the spatial library). CKAN
+supports different projections when creating this table, but it is recommended
+to use the default one (WGS 84 Latitude / Longitude - EPSG:4326).
+
+.. _PostGIS: http://www.postgis.org
+.. _GeoAlchemy: http://geoalchemy.org
+
+In order to get a dataset geometry imported into this table, an special extra
+must be defined, with its key named **spatial** (above). The value of this
+extra must be a valid GeoJSON_ geometry, for example::
+
+ {"type":"Polygon","coordinates":[[[2.05827, 49.8625],[2.05827, 55.7447], [-6.41736, 55.7447], [-6.41736, 49.8625], [2.05827, 49.8625]]]}
+
+Or::
+
+  { "type": "Point", "coordinates": [-3.145,53.078] }
+
+.. _GeoJSON: http://geojson.org 
+
+Every time a dataset is created, updated or deleted, the extension will
+synchronize the information stored in this extra with the geometry table.
+
+
+Spatial Query
+-------------
+
+The *spatial_query* plugin in the `Geospatial Extension`_ adds support for
+bounding box queries on the search API::
+
+  /api/2/search/package/geo?bbox=xmin,ymin,xmax,ymax
+  /api/2/search/package/geo?bbox=west,south,east,north
+
+For instance::
+
+  /api/2/search/package/geo?bbox=-3.224605,53.950255,-3.024175,54.129025
+
+Coordinates can be provided in a different projection system, if the spatial
+reference id is provided::
+
+  api/2/search/package/geo?bbox=320073,450947,332882,471045&crs=epsg:27700
+
+These requests will return the usual search API output::
+
+  {
+   "count": 2, 
+   "results": ["bb22bf62-6816-4b5f-97ea-ca8e8a4ce60c", "5016d4fe-acd8-42f7-bb7a-e0fb50a5e1fc"]
+  }
+
+Right now only bounding box searches are supported, but support for other types
+of search, as well as integration with the web frontend is planned.
+
+Dataset Extent Map
+------------------
+
+If you want to show a small map showing the geographic coverage of your dataset
+you need to enable the *spatial_query* and *dataset_extent_map* plugins of the
+`Geospatial Extension`_ (Check the README for requirements and installation).
+
+After enabling the plugin, if datasets contain a 'spatial' extra like the one
+described in the previous section, a map will be shown on the dataset details
+page.
+
+.. image:: http://farm8.staticflickr.com/7089/7072071969_9061f874b4_z.jpg
+
+The map is built using OpenLayers_ and shows cartography from the OpenStreetMap_.
+
+.. _OpenLayers: http://openlayers.org
+.. _OpenStreetMap: http://openstreetmap.org
+
+*Note*: Right now only geometries defined in WGS 84 Latitude / Longitude
+(EPSG:4326) projection are supported (e.g. the ones shown as example in this
+document).
+
+Previewing geospatial resources
+===============================
+
+WMS Previewing
+--------------
+
+To use it, enable the *wms_preview* plugin of the `Geospatial Extension`_.
+
+.. warning:: The WMS viewer is still experimental.
+
+The WMS (Web Map Service) previewing extension adds a light WMS client that
+allows to preview the different layers.  When installed, if the package has a
+resource with format *WMS* it will show a *View available WMS layer* link in
+the 'Resources' section. Clicking on it will open a light map viewer with a map
+list of available layers:
+
+.. image:: http://farm6.staticflickr.com/5111/6926001238_d2f0299eca_z.jpg
+
+
+Developer Notes
+===============
+
+The WMS viewer is built with OpenLayers_. There are various proposed
+improvements:
+
+* Base layer (we would need to handle different projections)
+* Show legends (WMS GetLegendGraphic)
+* Query layers (WMS GetFeatureInfo)
+* Support other layer types (KML, GeoJson...)
+
diff --git a/doc/index.rst b/doc/index.rst
index 87c82b2..4fe9aaf 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -38,6 +38,7 @@ Customizing and Extending
    filestore
    datastore
    background-tasks
+   geospatial
 
 Publishing Datasets
 ===================
@@ -49,6 +50,7 @@ Publishing Datasets
    loading-data
    authorization
    publisher-profile
+   geospatial
 
 The CKAN API
 ============


================================================================
  Commit: a162d172e4c7fff23ac274126e4f3ffa47ddcc51
      https://github.com/okfn/ckan/commit/a162d172e4c7fff23ac274126e4f3ffa47ddcc51
  Author: John Glover <j at johnglover.net>
  Date:   2012-04-16 (Mon, 16 Apr 2012)

  Changed paths:
    R ckan/migration/versions/052_add_group_logo.py
    A ckan/migration/versions/053_add_group_logo.py

  Log Message:
  -----------
  [2275][migration] change group logo migration number so it doesn't clash with master branch


diff --git a/ckan/migration/versions/052_add_group_logo.py b/ckan/migration/versions/052_add_group_logo.py
deleted file mode 100644
index 4f0a0d4..0000000
--- a/ckan/migration/versions/052_add_group_logo.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from sqlalchemy import *
-from migrate import *
-
-def upgrade(migrate_engine):
-    migrate_engine.execute('''
-        ALTER TABLE "group"
-            ADD COLUMN logo text;
-
-        ALTER TABLE group_revision
-            ADD COLUMN logo text;
-    '''
-    )
diff --git a/ckan/migration/versions/053_add_group_logo.py b/ckan/migration/versions/053_add_group_logo.py
new file mode 100644
index 0000000..4f0a0d4
--- /dev/null
+++ b/ckan/migration/versions/053_add_group_logo.py
@@ -0,0 +1,12 @@
+from sqlalchemy import *
+from migrate import *
+
+def upgrade(migrate_engine):
+    migrate_engine.execute('''
+        ALTER TABLE "group"
+            ADD COLUMN logo text;
+
+        ALTER TABLE group_revision
+            ADD COLUMN logo text;
+    '''
+    )


================================================================
  Commit: 98ba6b2c91103f33a63f12e7f2adea9a0a7c9605
      https://github.com/okfn/ckan/commit/98ba6b2c91103f33a63f12e7f2adea9a0a7c9605
  Author: John Glover <j at johnglover.net>
  Date:   2012-04-16 (Mon, 16 Apr 2012)

  Changed paths:
    M ckan/logic/schema.py
    M ckan/migration/versions/053_add_group_logo.py
    M ckan/model/group.py
    M ckan/templates/group/new_group_form.html
    M ckan/templates/group/read.html
    M ckan/tests/functional/test_group.py
    M ckan/tests/lib/test_dictization.py
    M ckan/tests/lib/test_dictization_schema.py

  Log Message:
  -----------
  [2275][model, schema, templates, tests] change group 'logo' to 'image_url'


diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py
index aabf757..b07d2bb 100644
--- a/ckan/logic/schema.py
+++ b/ckan/logic/schema.py
@@ -179,7 +179,7 @@ def default_group_schema():
         'name': [not_empty, unicode, name_validator, group_name_validator],
         'title': [ignore_missing, unicode],
         'description': [ignore_missing, unicode],
-        'logo': [ignore_missing, unicode],
+        'image_url': [ignore_missing, unicode],
         'type': [ignore_missing, unicode],
         'state': [ignore_not_group_admin, ignore_missing],
         'created': [ignore],
diff --git a/ckan/migration/versions/053_add_group_logo.py b/ckan/migration/versions/053_add_group_logo.py
index 4f0a0d4..7a31fb6 100644
--- a/ckan/migration/versions/053_add_group_logo.py
+++ b/ckan/migration/versions/053_add_group_logo.py
@@ -4,9 +4,9 @@
 def upgrade(migrate_engine):
     migrate_engine.execute('''
         ALTER TABLE "group"
-            ADD COLUMN logo text;
+            ADD COLUMN image_url text;
 
         ALTER TABLE group_revision
-            ADD COLUMN logo text;
+            ADD COLUMN image_url text;
     '''
     )
diff --git a/ckan/model/group.py b/ckan/model/group.py
index 78eae15..f622a7a 100644
--- a/ckan/model/group.py
+++ b/ckan/model/group.py
@@ -31,7 +31,7 @@
     Column('title', UnicodeText),
     Column('type', UnicodeText, nullable=False),
     Column('description', UnicodeText),
-    Column('logo', UnicodeText),
+    Column('image_url', UnicodeText),
     Column('created', DateTime, default=datetime.datetime.now),
     Column('approval_status', UnicodeText, default=u"approved"),
     )
@@ -79,12 +79,12 @@ class Group(vdm.sqlalchemy.RevisionedObjectMixin,
             vdm.sqlalchemy.StatefulObjectMixin,
             DomainObject):
 
-    def __init__(self, name=u'', title=u'', description=u'', logo=u'',
+    def __init__(self, name=u'', title=u'', description=u'', image_url=u'',
                  type=u'group', approval_status=u'approved'):
         self.name = name
         self.title = title
         self.description = description
-        self.logo = logo
+        self.image_url = image_url
         self.type = type
         self.approval_status= approval_status
 
diff --git a/ckan/templates/group/new_group_form.html b/ckan/templates/group/new_group_form.html
index c56239a..99d5388 100644
--- a/ckan/templates/group/new_group_form.html
+++ b/ckan/templates/group/new_group_form.html
@@ -44,10 +44,10 @@
     </div>
   </div>
   <div class="control-group">
-    <label for="name" class="control-label">Logo URL:</label>
+    <label for="name" class="control-label">Image URL:</label>
     <div class="controls">
-      <input id="logo" name="logo" type="text" value="${data.get('logo', '')}"/>
-      <p>The URL for the logo that is associated with this group.</p>
+      <input id="image_url" name="image_url" type="text" value="${data.get('image_url', '')}"/>
+      <p>The URL for the image that is associated with this group.</p>
     </div>
   </div>
   <div class="state-field control-group" py:if="c.is_sysadmin or c.auth_for_change_state">
diff --git a/ckan/templates/group/read.html b/ckan/templates/group/read.html
index 5cf7bb5..7b01fe5 100644
--- a/ckan/templates/group/read.html
+++ b/ckan/templates/group/read.html
@@ -6,8 +6,8 @@
   <xi:include href="../facets.html" />
   <py:def function="page_title">${c.group.display_name}</py:def>
   <py:def function="page_heading">${c.group.display_name}</py:def>
-  <py:if test="c.group.logo">
-    <py:def function="page_logo">${c.group.logo}</py:def>
+  <py:if test="c.group.image_url">
+    <py:def function="page_logo">${c.group.image_url}</py:def>
   </py:if>
 
   <py:match path="primarysidebar">
diff --git a/ckan/tests/functional/test_group.py b/ckan/tests/functional/test_group.py
index c629cbd..e71cd28 100644
--- a/ckan/tests/functional/test_group.py
+++ b/ckan/tests/functional/test_group.py
@@ -259,19 +259,19 @@ def test_edit_plugin_hook(self):
         assert plugin.calls['edit'] == 1, plugin.calls
         plugins.unload(plugin)
 
-    def test_edit_logo(self):
+    def test_edit_image_url(self):
         group = model.Group.by_name(self.groupname)
         offset = url_for(controller='group', action='edit', id=self.groupname)
         res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'})
 
         form = res.forms['group-edit']
-        logo_url = u'http://url.to/logo'
-        form['logo'] = logo_url
+        image_url = u'http://url.to/image_url'
+        form['image_url'] = image_url
         res = form.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'})
 
         model.Session.remove()
         group = model.Group.by_name(self.groupname)
-        assert group.logo == logo_url, group
+        assert group.image_url == image_url, group
 
     def test_edit_non_existent(self):
         name = u'group_does_not_exist'
diff --git a/ckan/tests/lib/test_dictization.py b/ckan/tests/lib/test_dictization.py
index e6e8ae3..c6ceb0b 100644
--- a/ckan/tests/lib/test_dictization.py
+++ b/ckan/tests/lib/test_dictization.py
@@ -42,7 +42,7 @@ def setup_class(cls):
             'groups': [{'description': u'These are books that David likes.',
                         'name': u'david',
                         'capacity': 'public',
-                        'logo': u'',
+                        'image_url': u'',
                         'type': u'group',
                         'state': u'active',
                         'title': u"Dave's books",
@@ -50,7 +50,7 @@ def setup_class(cls):
                        {'description': u'Roger likes these books.',
                         'name': u'roger',
                         'capacity': 'public',
-                        'logo': u'',
+                        'image_url': u'',
                         'type': u'group',
                         'state': u'active',
                         'title': u"Roger's books",
@@ -871,7 +871,7 @@ def test_16_group_dictized(self):
                     'groups': [{'description': u'',
                                'capacity' : 'public',
                                'display_name': u'simple',
-                               'logo': u'',
+                               'image_url': u'',
                                'name': u'simple',
                                'packages': 0,
                                'state': u'active',
@@ -890,7 +890,7 @@ def test_16_group_dictized(self):
                               'reset_key': None}],
                     'name': u'help',
                     'display_name': u'help',
-                    'logo': u'',
+                    'image_url': u'',
                     'packages': [{'author': None,
                                   'author_email': None,
                                   'license_id': u'other-open',
diff --git a/ckan/tests/lib/test_dictization_schema.py b/ckan/tests/lib/test_dictization_schema.py
index f0cf8e7..a004b1e 100644
--- a/ckan/tests/lib/test_dictization_schema.py
+++ b/ckan/tests/lib/test_dictization_schema.py
@@ -149,7 +149,7 @@ def test_2_group_schema(self):
                                  'id': group.id,
                                  'name': u'david',
                                  'type': u'group',
-                                 'logo': u'',
+                                 'image_url': u'',
                                  'packages': sorted([{'id': group_pack[0].id,
                                                     'name': group_pack[0].name,
                                                     'title': group_pack[0].title},


================================================================
  Commit: 114ecacef01cb0a698e7767fba2853b875696687
      https://github.com/okfn/ckan/commit/114ecacef01cb0a698e7767fba2853b875696687
  Author: John Glover <j at johnglover.net>
  Date:   2012-04-16 (Mon, 16 Apr 2012)

  Changed paths:
    M ckan/logic/schema.py
    A ckan/migration/versions/053_add_group_logo.py
    M ckan/model/group.py
    M ckan/public/css/style.css
    M ckan/templates/group/new_group_form.html
    M ckan/templates/group/read.html
    M ckan/templates/layout_base.html
    M ckan/tests/functional/test_group.py
    M ckan/tests/lib/test_dictization.py
    M ckan/tests/lib/test_dictization_schema.py

  Log Message:
  -----------
  Merge branch 'feature-2275-group-logo'


diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py
index e77b894..b07d2bb 100644
--- a/ckan/logic/schema.py
+++ b/ckan/logic/schema.py
@@ -179,6 +179,7 @@ def default_group_schema():
         'name': [not_empty, unicode, name_validator, group_name_validator],
         'title': [ignore_missing, unicode],
         'description': [ignore_missing, unicode],
+        'image_url': [ignore_missing, unicode],
         'type': [ignore_missing, unicode],
         'state': [ignore_not_group_admin, ignore_missing],
         'created': [ignore],
diff --git a/ckan/migration/versions/053_add_group_logo.py b/ckan/migration/versions/053_add_group_logo.py
new file mode 100644
index 0000000..7a31fb6
--- /dev/null
+++ b/ckan/migration/versions/053_add_group_logo.py
@@ -0,0 +1,12 @@
+from sqlalchemy import *
+from migrate import *
+
+def upgrade(migrate_engine):
+    migrate_engine.execute('''
+        ALTER TABLE "group"
+            ADD COLUMN image_url text;
+
+        ALTER TABLE group_revision
+            ADD COLUMN image_url text;
+    '''
+    )
diff --git a/ckan/model/group.py b/ckan/model/group.py
index b43ee10..f622a7a 100644
--- a/ckan/model/group.py
+++ b/ckan/model/group.py
@@ -31,6 +31,7 @@
     Column('title', UnicodeText),
     Column('type', UnicodeText, nullable=False),
     Column('description', UnicodeText),
+    Column('image_url', UnicodeText),
     Column('created', DateTime, default=datetime.datetime.now),
     Column('approval_status', UnicodeText, default=u"approved"),
     )
@@ -78,11 +79,12 @@ class Group(vdm.sqlalchemy.RevisionedObjectMixin,
             vdm.sqlalchemy.StatefulObjectMixin,
             DomainObject):
 
-    def __init__(self, name=u'', title=u'', description=u'',
-                 type=u'group', approval_status=u'approved' ):
+    def __init__(self, name=u'', title=u'', description=u'', image_url=u'',
+                 type=u'group', approval_status=u'approved'):
         self.name = name
         self.title = title
         self.description = description
+        self.image_url = image_url
         self.type = type
         self.approval_status= approval_status
 
diff --git a/ckan/public/css/style.css b/ckan/public/css/style.css
index 354734e..981156e 100644
--- a/ckan/public/css/style.css
+++ b/ckan/public/css/style.css
@@ -196,6 +196,11 @@ tbody tr:nth-child(odd) td, tbody tr.odd td {
   font-size: 2.2em;
   font-weight: normal;
 }
+#page-logo {
+  max-width: 36px;
+  max-height: 36px;
+  margin-right: 5px;
+}
 .hover-for-help {
   position: relative;
 }
diff --git a/ckan/templates/group/new_group_form.html b/ckan/templates/group/new_group_form.html
index ad24ea3..99d5388 100644
--- a/ckan/templates/group/new_group_form.html
+++ b/ckan/templates/group/new_group_form.html
@@ -43,6 +43,13 @@
       ${markdown_editor('description', data.get('description'), 'notes', _('Start with a summary sentence ...'))}
     </div>
   </div>
+  <div class="control-group">
+    <label for="name" class="control-label">Image URL:</label>
+    <div class="controls">
+      <input id="image_url" name="image_url" type="text" value="${data.get('image_url', '')}"/>
+      <p>The URL for the image that is associated with this group.</p>
+    </div>
+  </div>
   <div class="state-field control-group" py:if="c.is_sysadmin or c.auth_for_change_state">
     <label for="" class="control-label">State</label>
     <div class="controls">
diff --git a/ckan/templates/group/read.html b/ckan/templates/group/read.html
index 69314b4..7b01fe5 100644
--- a/ckan/templates/group/read.html
+++ b/ckan/templates/group/read.html
@@ -6,6 +6,9 @@
   <xi:include href="../facets.html" />
   <py:def function="page_title">${c.group.display_name}</py:def>
   <py:def function="page_heading">${c.group.display_name}</py:def>
+  <py:if test="c.group.image_url">
+    <py:def function="page_logo">${c.group.image_url}</py:def>
+  </py:if>
 
   <py:match path="primarysidebar">
   
diff --git a/ckan/templates/layout_base.html b/ckan/templates/layout_base.html
index 3b090f1..1518d72 100644
--- a/ckan/templates/layout_base.html
+++ b/ckan/templates/layout_base.html
@@ -90,7 +90,10 @@
     </py:with>
 
     <div id="main" class="container" role="main">
-      <h1 py:if="defined('page_heading')" class="page_heading">${page_heading()}</h1>
+      <h1 py:if="defined('page_heading')" class="page_heading">
+        <img py:if="defined('page_logo')" id="page-logo" src="${page_logo()}" alt="Page Logo" />
+        ${page_heading()}
+      </h1>
       <div class="row">
         <div class="span12">
           <div id="minornavigation">
diff --git a/ckan/tests/functional/test_group.py b/ckan/tests/functional/test_group.py
index 8b452a6..e71cd28 100644
--- a/ckan/tests/functional/test_group.py
+++ b/ckan/tests/functional/test_group.py
@@ -72,7 +72,7 @@ def test_mainmenu(self):
     def test_index(self):
         offset = url_for(controller='group', action='index')
         res = self.app.get(offset)
-        assert '<h1 class="page_heading">Groups' in res, res
+        assert re.search('<h1(.*)>\s*Groups', res.body)
         groupname = 'david'
         group = model.Group.by_name(unicode(groupname))
         group_title = group.title
@@ -259,6 +259,20 @@ def test_edit_plugin_hook(self):
         assert plugin.calls['edit'] == 1, plugin.calls
         plugins.unload(plugin)
 
+    def test_edit_image_url(self):
+        group = model.Group.by_name(self.groupname)
+        offset = url_for(controller='group', action='edit', id=self.groupname)
+        res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER': 'russianfan'})
+
+        form = res.forms['group-edit']
+        image_url = u'http://url.to/image_url'
+        form['image_url'] = image_url
+        res = form.submit('save', status=302, extra_environ={'REMOTE_USER': 'russianfan'})
+
+        model.Session.remove()
+        group = model.Group.by_name(self.groupname)
+        assert group.image_url == image_url, group
+
     def test_edit_non_existent(self):
         name = u'group_does_not_exist'
         offset = url_for(controller='group', action='edit', id=name)
diff --git a/ckan/tests/lib/test_dictization.py b/ckan/tests/lib/test_dictization.py
index ba8ec69..c6ceb0b 100644
--- a/ckan/tests/lib/test_dictization.py
+++ b/ckan/tests/lib/test_dictization.py
@@ -42,19 +42,19 @@ def setup_class(cls):
             'groups': [{'description': u'These are books that David likes.',
                         'name': u'david',
                         'capacity': 'public',
+                        'image_url': u'',
                         'type': u'group',
                         'state': u'active',
                         'title': u"Dave's books",
-                        "approval_status": u"approved",
-                        'capacity': u'public'},
+                        "approval_status": u"approved"},
                        {'description': u'Roger likes these books.',
                         'name': u'roger',
                         'capacity': 'public',
+                        'image_url': u'',
                         'type': u'group',
                         'state': u'active',
                         'title': u"Roger's books",
-                        "approval_status": u"approved",
-                        'capacity': u'public'}],
+                        "approval_status": u"approved"}],
             'isopen': True,
             'license_id': u'other-open',
             'license_title': u'Other (Open)',
@@ -212,7 +212,7 @@ def test_01_dictize_main_objects_simple(self):
     def test_02_package_dictize(self):
 
         context = {"model": model,
-                 "session": model.Session}
+                   "session": model.Session}
 
         model.Session.remove()
         pkg = model.Session.query(model.Package).filter_by(name='annakarenina').first()
@@ -864,13 +864,14 @@ def test_16_group_dictized(self):
 
         group_dictized = group_dictize(group, context)
 
-        expected =  {'description': u'',
+        expected = {'description': u'',
                     'extras': [{'key': u'genre', 'state': u'active', 'value': u'"horror"'},
                                {'key': u'media', 'state': u'active', 'value': u'"dvd"'}],
                     'tags': [{'capacity': 'public', 'name': u'russian'}],
                     'groups': [{'description': u'',
                                'capacity' : 'public',
                                'display_name': u'simple',
+                               'image_url': u'',
                                'name': u'simple',
                                'packages': 0,
                                'state': u'active',
@@ -889,6 +890,7 @@ def test_16_group_dictized(self):
                               'reset_key': None}],
                     'name': u'help',
                     'display_name': u'help',
+                    'image_url': u'',
                     'packages': [{'author': None,
                                   'author_email': None,
                                   'license_id': u'other-open',
diff --git a/ckan/tests/lib/test_dictization_schema.py b/ckan/tests/lib/test_dictization_schema.py
index 13f14e1..a004b1e 100644
--- a/ckan/tests/lib/test_dictization_schema.py
+++ b/ckan/tests/lib/test_dictization_schema.py
@@ -149,6 +149,7 @@ def test_2_group_schema(self):
                                  'id': group.id,
                                  'name': u'david',
                                  'type': u'group',
+                                 'image_url': u'',
                                  'packages': sorted([{'id': group_pack[0].id,
                                                     'name': group_pack[0].name,
                                                     'title': group_pack[0].title},


================================================================
Compare: https://github.com/okfn/ckan/compare/0ace32f...114ecac


More information about the ckan-changes mailing list