def _is_longitude_field(self, value, context):
        """Ensure this field can be used for longitudes"""
        # We can't use datastore_search for this, as we need operators
        db = _get_engine()
        metadata = MetaData()
        table = Table(context['resource'].id, metadata, Column(value, Numeric))

        query = select([func.count(1).label('count')], from_obj=table)
        query = query.where(not_(table.c[value] == None))
        query = query.where(or_(cast(table.c[value], Numeric) < -180, cast(table.c[value], Numeric) > 180))
        with db.begin() as connection:
            try:
                query_result = connection.execute(query)
            except DataError as e:
                raise p.toolkit.Invalid(_('Longitude field must contain numeric data'))

            row = query_result.fetchone()
            query_result.close()
            if row['count'] > 0:
                raise p.toolkit.Invalid(_('Longitude field must be between -180 and 180'))
        return value
    def map_info(self):
        """Controller action that returns metadata about a given map.

        As a side effect this will set the content type to application/json

        @return: A JSON encoded string representing the metadata
        """
        # Specific parameters
        fetch_id = request.params.get('fetch_id')
        tile_url_base = 'http://{host}:{port}/database/{database}/table/{table}'.format(
            host=config['tiledmap.windshaft.host'],
            port=config['tiledmap.windshaft.port'],
            database=_get_engine().url.database,
            table=self.resource_id
        )

        ## Ensure we have at least one map style
        if not self.view['enable_plot_map'] and not self.view['enable_grid_map'] and not self.view['enable_heat_map']:
            return json.dumps({
                'geospatial': False,
                'fetch_id': fetch_id
            })

        # Prepare result
        quick_info_template_name = "{base}.{format}.mustache".format(
            base=self.quick_info_template,
            format=str(self.resource['format']).lower()
        )
        if not find_template(quick_info_template_name):
            quick_info_template_name = self.quick_info_template + '.mustache'
        info_template_name = "{base}.{format}.mustache".format(
            base=self.info_template,
            format=str(self.resource['format']).lower()
        )
        if not find_template(info_template_name):
            info_template_name = self.info_template + '.mustache'


        quick_info_template = toolkit.render(quick_info_template_name, {
            'title': self.info_title,
            'fields': self.info_fields
        })
        info_template = toolkit.render(info_template_name, {
            'title': self.info_title,
            'fields': self.info_fields,
            'overlapping_records_view': self.view['overlapping_records_view']
        })
        result = {
            'geospatial': True,
            'geom_count': 0,
            'total_count': 0,
            'bounds': ((83, -170), (-83, 170)),
            'zoom_bounds': {
                'min': int(config['tiledmap.zoom_bounds.min']),
                'max': int(config['tiledmap.zoom_bounds.max'])
            },
            'initial_zoom': {
                'min': int(config['tiledmap.initial_zoom.min']),
                'max': int(config['tiledmap.initial_zoom.max'])
            },
            'tile_layer': {
                'url': config['tiledmap.tile_layer.url'],
                'opacity': float(config['tiledmap.tile_layer.opacity'])
            },
            'repeat_map': self.repeat_map,
            'map_styles': {
            },
            'control_options': {
                'fullScreen': {
                    'position': 'topright'
                },
                'drawShape': {
                    'draw': {
                        'polyline': False,
                        'marker': False,
                        'circle': False,
                        'country': True,
                        'polygon': {
                            'allowIntersection': False,
                            'shapeOptions': {
                                'stroke': True,
                                'color': '#F44',
                                'weight': 5,
                                'opacity': 0.5,
                                'fill': True,
                                'fillColor': '#F44',
                                'fillOpacity': 0.1
                            }
                        }
                    },
                    'position': 'topleft'
                },
                'selectCountry': {
                    'draw': {
                        'fill': '#F44',
                        'fill-opacity': '0.1',
                        'stroke': '#F44',
                        'stroke-opacity': '0.5'
                    }
                },
                'mapType': {
                    'position': 'bottomleft'
                },
                'miniMap': {
                    'position': 'bottomright',
                    'tile_layer': {
                        'url': config['tiledmap.tile_layer.url']
                    },
                    'zoomLevelFixed': 1,
                    #'zoomLevelOffset': -10,
                    'toggleDisplay': True,
                    'viewport': {
                        'marker_zoom': 8,
                        'rect': {
                            'weight': 1,
                            'color': '#00F',
                            'opacity': 1,
                            'fill': False
                        },
                        'marker': {
                            'weight': 1,
                            'color': '#00F',
                            'opacity': 1,
                            'radius': 3,
                            'fillColor': '#00F',
                            'fillOpacity': 0.2
                        }
                    }
                }
            },
            'plugin_options': {
                'tooltipInfo': {
                    'count_field': '_tiledmap_count',
                    'template': quick_info_template,
                },
                'tooltipCount': {
                    'count_field': '_tiledmap_count'
                },
                'pointInfo': {
                    'template': info_template,
                    'count_field': '_tiledmap_count'
                }
            },
            'fetch_id': fetch_id
        }

        if self.view['enable_heat_map']:
            result['map_styles']['heatmap'] = {
                'name': _('Heat Map'),
                'icon': '<i class="fa fa-fire"></i>',
                'controls': ['drawShape', 'mapType', 'fullScreen', 'miniMap'],
                'has_grid': False,
                'tile_source': {
                    'url': tile_url_base + '/{z}/{x}/{y}.png',
                    'params': {
                        'intensity': config['tiledmap.style.heatmap.intensity'],
                    }
                },
            }
            result['map_style'] = 'heatmap'

        if self.view['enable_grid_map']:
            result['map_styles']['gridded'] = {
                'name': _('Grid Map'),
                'icon': '<i class="fa fa-th"></i>',
                'controls': ['drawShape', 'mapType', 'fullScreen', 'miniMap'],
                'plugins': ['tooltipCount'],
                'has_grid': self.view['enable_utf_grid'],
                'grid_resolution': int(config['tiledmap.style.plot.grid_resolution']),
                'tile_source': {
                    'url': tile_url_base + '/{z}/{x}/{y}.png',
                    'params': {
                        'base_color': config['tiledmap.style.gridded.base_color']
                    }
                },
                'grid_source': {
                    'url': tile_url_base + '/{z}/{x}/{y}.grid.json',
                    'params': {
                        'interactivity': ','.join(self.query_fields)
                    }
                }
            }
            result['map_style'] = 'gridded'

        if self.view['enable_plot_map']:
            result['map_styles']['plot'] = {
                'name': _('Plot Map'),
                'icon': '<i class="fa fa-dot-circle-o"></i>',
                'controls': ['drawShape', 'mapType', 'fullScreen', 'miniMap'],
                'plugins': ['tooltipInfo', 'pointInfo'],
                'has_grid': self.view['enable_utf_grid'],
                'grid_resolution': int(config['tiledmap.style.plot.grid_resolution']),
                'tile_source': {
                    'url': tile_url_base + '/{z}/{x}/{y}.png',
                    'params': {
                        'fill_color': config['tiledmap.style.plot.fill_color'],
                        'line_color': config['tiledmap.style.plot.line_color']
                    }
                },
                'grid_source': {
                    'url': tile_url_base + '/{z}/{x}/{y}.grid.json',
                    'params': {
                        'interactivity': ','.join(self.query_fields)
                    }
                }
            }
            result['map_style'] = 'plot'

        # Get query extent and count
        info = toolkit.get_action('datastore_query_extent')({}, {
            'resource_id': self.resource_id,
            'filters': self._get_request_filters(),
            'limit': 1,
            'q': urllib.unquote(request.params.get('q', '')),
            'fields': '_id'
        })
        result['total_count'] = info['total_count']
        result['geom_count'] = info['geom_count']
        if info['bounds']:
            result['bounds'] = info['bounds']

        response.headers['Content-type'] = 'application/json'
        return json.dumps(result)