Commit a9dc5447 authored by David Baumgold's avatar David Baumgold

Added geoa contrib module for geoalchemy2 integration

Still needs docs and tests
parent b778848a
......@@ -2,7 +2,7 @@ import os.path as op
from functools import wraps
from flask import Blueprint, render_template, abort, g, url_for
from flask import Blueprint, current_app, render_template, abort, g, url_for
from flask.ext.admin import babel
from flask.ext.admin._compat import with_metaclass
from flask.ext.admin import helpers as h
......@@ -275,6 +275,9 @@ class BaseView(with_metaclass(AdminViewMeta, BaseViewClass)):
# Expose get_url helper
kwargs['get_url'] = self.get_url
# Expose config info
kwargs['config'] = current_app.config
# Contribute extra arguments
kwargs.update(self._template_args)
......
try:
import geoalchemy2
import shapely
except ImportError:
raise Exception('Please install geoalchemy2 and shapely in order to use geoalchemy integration')
from .view import ModelView
import json
from wtforms.fields import TextAreaField
from shapely.geometry import shape, mapping
from .widgets import LeafletWidget
class JSONField(TextAreaField):
def _value(self):
if self.raw_data:
return self.raw_data[0]
if self.data:
return self.to_json(self.data)
return ""
def process_formdata(self, valuelist):
if valuelist:
value = valuelist[0]
if not value:
self.data = None
return
try:
self.data = self.from_json(value)
except ValueError:
self.data = None
raise ValueError(self.gettext('Invalid JSON'))
def to_json(self, obj):
return json.dumps(obj)
def from_json(self, data):
return json.loads(data)
class GeoJSONField(JSONField):
widget = LeafletWidget()
def __init__(self, label=None, validators=None, geometry_type="GEOMETRY", **kwargs):
super(GeoJSONField, self).__init__(label, validators, **kwargs)
self.geometry_type = geometry_type.upper()
def _value(self):
if self.raw_data:
return self.raw_data[0]
if self.data:
self.data = mapping(self.data)
return super(GeoJSONField, self)._value()
def process_formdata(self, valuelist):
super(GeoJSONField, self).process_formdata(valuelist)
if self.data:
self.data = shape(self.data)
from flask.ext.admin.model.form import converts
from flask.ext.admin.contrib.sqla.form import AdminModelConverter as SQLAAdminConverter
from .fields import GeoJSONField
class AdminModelConverter(SQLAAdminConverter):
@converts('Geometry')
def convert_geom(self, column, field_args, **extra):
field_args['geometry_type'] = column.type.geometry_type
return GeoJSONField(**field_args)
from geoalchemy2 import Geometry as BaseGeometry
from geoalchemy2.shape import to_shape
class Geometry(BaseGeometry):
"""
PostGIS datatype that can convert directly to/from Shapely objects,
without worrying about WKTElements or WKBElements.
"""
def result_processor(self, dialect, coltype):
to_wkbelement = super(Geometry, self).result_processor(dialect, coltype)
def process(value):
if value:
return to_shape(to_wkbelement(value))
else:
return None
return process
def bind_processor(self, dialect):
from_wktelement = super(Geometry, self).bind_processor(dialect)
def process(value):
if value:
return from_wktelement(value.wkt)
else:
return None
return process
from flask.ext.admin.contrib.sqla.typefmt import DEFAULT_FORMATTERS as BASE_FORMATTERS
import json
from jinja2 import Markup
from wtforms.widgets import html_params
from shapely.geometry import mapping
from shapely.geometry.base import BaseGeometry
def geom_formatter(view, value):
params = html_params(**{
"data-role": "leaflet",
"disabled": "disabled",
"data-width": 100,
"data-height": 70,
"data-geometry-type": value.geom_type,
"data-zoom": 15,
})
geojson = json.dumps(mapping(value))
return Markup('<textarea %s>%s</textarea>' % (params, geojson))
DEFAULT_FORMATTERS = BASE_FORMATTERS.copy()
DEFAULT_FORMATTERS[BaseGeometry] = geom_formatter
from flask.ext.admin.contrib.sqla import ModelView as SQLAModelView
from flask.ext.admin.contrib.geoa import form, typefmt
class ModelView(SQLAModelView):
model_form_converter = form.AdminModelConverter
column_type_formatters = typefmt.DEFAULT_FORMATTERS
from wtforms.widgets import TextArea
def lat(pt):
return getattr(pt, "lat", getattr(pt, "x", pt[0]))
def lng(pt):
return getattr(pt, "lng", getattr(pt, "y", pt[1]))
class LeafletWidget(TextArea):
"""
`Leaflet <http://leafletjs.com/>`_ styled map widget. Inherits from
`TextArea` so that geographic data can be stored via the <textarea>
(and edited there if the user's browser does not have Javascript).
You must include leaflet.js, form.js and leaflet stylesheet for it to
work. You also need leaflet.draw.js (and its stylesheet) for it to be
editable.
"""
def __init__(
self, width=300, height=300, center=None,
zoom=None, min_zoom=None, max_zoom=None, max_bounds=None):
self.width = width
self.height = height
self.center = center
self.zoom = zoom
self.min_zoom = min_zoom
self.max_zoom = max_zoom
self.max_bounds = max_bounds
def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', 'leaflet')
gtype = getattr(field, "geometry_type", "GEOMETRY")
kwargs.setdefault('data-geometry-type', gtype)
# set optional values from constructor
if self.width:
kwargs["data-width"] = self.width
if self.height:
kwargs["data-height"] = self.height
if self.center:
kwargs["data-lat"] = lat(self.center)
kwargs["data-lng"] = lng(self.center)
if self.zoom:
kwargs["data-zoom"] = self.zoom
if self.min_zoom:
kwargs["data-min-zoom"] = self.min_zoom
if self.max_zoom:
kwargs["data-max-zoom"] = self.max_zoom
if self.max_bounds:
if getattr(self.max_bounds, "bounds"):
# this is a Shapely geometric object
minx, miny, maxx, maxy = self.max_bounds.bounds
elif len(self.max_bounds) == 4:
# this is a list of four values
minx, miny, maxx, maxy = self.max_bounds
else:
# this is a list of two points
minx = lat(self.max_bounds[0])
miny = lng(self.max_bounds[0])
maxx = lat(self.max_bounds[1])
maxy = lng(self.max_bounds[1])
kwargs["data-max-bounds-sw-lat"] = minx
kwargs["data-max-bounds-sw-lng"] = miny
kwargs["data-max-bounds-ne-lat"] = maxx
kwargs["data-max-bounds-ne-lng"] = maxy
return super(LeafletWidget, self).__call__(field, **kwargs)
try:
import pymongo
except ImportError:
raise Exception('Please install pymongo in order to use peewee integration')
raise Exception('Please install pymongo in order to use pymongo integration')
from .view import ModelView
......@@ -400,7 +400,7 @@ class BaseModelView(BaseView, ActionsMixin):
}
Changing the format of a DateTimeField will require changes to both form_widget_args and form_args.
Example::
form_args = dict(
......@@ -1197,7 +1197,11 @@ class BaseModelView(BaseView, ActionsMixin):
if choices_map:
return choices_map.get(value) or value
type_fmt = self.column_type_formatters.get(type(value))
type_fmt = None
for typeobj, formatter in self.column_type_formatters.items():
if isinstance(value, typeobj):
type_fmt = formatter
break
if type_fmt is not None:
value = type_fmt(self, value)
......
......@@ -69,6 +69,163 @@
$el.select2(opts);
}
/**
* Process Leaflet (map) widget
*/
function processLeafletWidget($el, name) {
if (!window.MAPBOX_MAP_ID) {
console.error("You must set MAPBOX_MAP_ID in your Flask settings to use the map widget");
return false;
}
var geometryType = $el.data("geometry-type")
if (geometryType) {
geometryType = geometryType.toUpperCase();
} else {
geometryType = "GEOMETRY";
}
var multiple = geometryType.lastIndexOf("MULTI", geometryType) === 0;
var editable = ! $el.is(":disabled");
var $map = $("<div>").width($el.data("width")).height($el.data("height"));
$el.after($map).hide();
var center = null;
if($el.data("lat") && $el.data("lng")) {
center = L.latLng($el.data("lat"), $el.data("lng"));
}
var maxBounds = null;
if ($el.data("max-bounds-sw-lat") && $el.data("max-bounds-sw-lng") &&
$el.data("max-bounds-ne-lat") && $el.data("max-bounds-ne-lng"))
{
maxBounds = L.latLngBounds(
L.latLng($el.data("max-bounds-sw-lat"), $el.data("max-bounds-sw-lng")),
L.latLng($el.data("max-bounds-ne-lat"), $el.data("max-bounds-ne-lng"))
)
}
var editableLayers;
if ($el.val()) {
editableLayers = new L.geoJson(JSON.parse($el.val()));
center = center || editableLayers.getBounds().getCenter();
} else {
editableLayers = new L.geoJson();
}
var mapOptions = {
center: center,
zoom: $el.data("zoom") || 12,
minZoom: $el.data("min-zoom"),
maxZoom: $el.data("max-zoom"),
maxBounds: maxBounds
}
if (!editable) {
mapOptions.dragging = false;
mapOptions.touchzoom = false;
mapOptions.scrollWheelZoom = false;
mapOptions.doubleClickZoom = false;
mapOptions.boxZoom = false;
mapOptions.tap = false;
mapOptions.keyboard = false;
mapOptions.zoomControl = false;
}
// only show attributions if the map is big enough
// (otherwise, it gets in the way)
if ($map.width() * $map.height() < 10000) {
mapOptions.attributionControl = false;
}
var map = L.map($map.get(0), mapOptions)
map.addLayer(editableLayers);
if (center) {
// if we have more than one point, make the map show everything
var bounds = editableLayers.getBounds()
if (!bounds.getNorthEast().equals(bounds.getSouthWest())) {
map.fitBounds(bounds);
}
} else {
// look up user's location by IP address
$.getJSON("http://ip-api.com/json/?callback=?", function(data) {
map.setView([data["lat"], data["lon"]], 12);
});
}
// set up tiles
L.tileLayer('http://{s}.tiles.mapbox.com/v3/'+MAPBOX_MAP_ID+'/{z}/{x}/{y}.png', {
attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="http://mapbox.com">Mapbox</a>',
maxZoom: 18
}).addTo(map);
// everything below here is to set up editing, so if we're not editable,
// we can just return early.
if (!editable) {
return true;
}
// set up Leaflet.draw editor
var drawOptions = {
draw: {
// circles are not geometries in geojson
circle: false
},
edit: {
featureGroup: editableLayers
}
}
if ($.inArray(geometryType, ["POINT", "MULTIPOINT"]) > -1) {
drawOptions.draw.polyline = false;
drawOptions.draw.polygon = false;
drawOptions.draw.rectangle = false;
} else if ($.inArray(geometryType, ["LINESTRING", "MULTILINESTRING"]) > -1) {
drawOptions.draw.marker = false;
drawOptions.draw.polygon = false;
drawOptions.draw.rectangle = false;
} else if ($.inArray(geometryType, ["POLYGON", "MULTIPOLYGON"]) > -1) {
drawOptions.draw.marker = false;
drawOptions.draw.polyline = false;
}
var drawControl = new L.Control.Draw(drawOptions);
map.addControl(drawControl);
// save when the editableLayers are edited
var saveToTextArea = function() {
var geo = editableLayers.toGeoJSON();
if (geo.features.length === 0) {
$el.val("");
return true
}
if (multiple) {
var coords = $.map(geo.features, function(feature) {
return [feature.geometry.coordinates];
})
geo = {
"type": geometryType,
"coordinates": coords
}
} else {
geo = geo.features[0].geometry;
}
$el.val(JSON.stringify(geo));
}
// handle creation
map.on('draw:created', function (e) {
if (!multiple) {
editableLayers.clearLayers();
}
editableLayers.addLayer(e.layer);
saveToTextArea();
})
map.on('draw:edited', saveToTextArea);
map.on('draw:deleted', saveToTextArea);
}
/**
* Process data-role attribute for the given input element. Feel free to override
*
......@@ -197,6 +354,9 @@
$container.find('.calendar-date').remove();
});
return true;
case 'leaflet':
processLeafletWidget($el, name);
return true;
}
};
......
This diff is collapsed.
/* ================================================================== */
/* Toolbars
/* ================================================================== */
.leaflet-draw-section {
position: relative;
}
.leaflet-draw-toolbar {
margin-top: 12px;
}
.leaflet-draw-toolbar-top {
margin-top: 0;
}
.leaflet-draw-toolbar-notop a:first-child {
border-top-right-radius: 0;
}
.leaflet-draw-toolbar-nobottom a:last-child {
border-bottom-right-radius: 0;
}
.leaflet-draw-toolbar a {
background-image: url('images/spritesheet.png');
background-repeat: no-repeat;
}
.leaflet-retina .leaflet-draw-toolbar a {
background-image: url('images/spritesheet-2x.png');
background-size: 270px 30px;
}
.leaflet-draw a {
display: block;
text-align: center;
text-decoration: none;
}
/* ================================================================== */
/* Toolbar actions menu
/* ================================================================== */
.leaflet-draw-actions {
display: none;
list-style: none;
margin: 0;
padding: 0;
position: absolute;
left: 26px; /* leaflet-draw-toolbar.left + leaflet-draw-toolbar.width */
top: 0;
white-space: nowrap;
}
.leaflet-right .leaflet-draw-actions {
right:26px;
left:auto;
}
.leaflet-draw-actions li {
display: inline-block;
}
.leaflet-draw-actions li:first-child a {
border-left: none;
}
.leaflet-draw-actions li:last-child a {
-webkit-border-radius: 0 4px 4px 0;
border-radius: 0 4px 4px 0;
}
.leaflet-right .leaflet-draw-actions li:last-child a {
-webkit-border-radius: 0;
border-radius: 0;
}
.leaflet-right .leaflet-draw-actions li:first-child a {
-webkit-border-radius: 4px 0 0 4px;
border-radius: 4px 0 0 4px;
}
.leaflet-draw-actions a {
background-color: #919187;
border-left: 1px solid #AAA;
color: #FFF;
font: 11px/19px "Helvetica Neue", Arial, Helvetica, sans-serif;
line-height: 28px;
text-decoration: none;
padding-left: 10px;
padding-right: 10px;
height: 28px;
}
.leaflet-draw-actions-bottom {
margin-top: 0;
}
.leaflet-draw-actions-top {
margin-top: 1px;
}
.leaflet-draw-actions-top a,
.leaflet-draw-actions-bottom a {
height: 27px;
line-height: 27px;
}
.leaflet-draw-actions a:hover {
background-color: #A0A098;
}
.leaflet-draw-actions-top.leaflet-draw-actions-bottom a {
height: 26px;
line-height: 26px;
}
/* ================================================================== */
/* Draw toolbar
/* ================================================================== */
.leaflet-draw-toolbar .leaflet-draw-draw-polyline {
background-position: -2px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-draw-polygon {
background-position: -31px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-draw-rectangle {
background-position: -62px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-draw-circle {
background-position: -92px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-draw-marker {
background-position: -122px -2px;
}
/* ================================================================== */
/* Edit toolbar
/* ================================================================== */
.leaflet-draw-toolbar .leaflet-draw-edit-edit {
background-position: -152px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-edit-remove {
background-position: -182px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled {
background-position: -212px -2px;
}
.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled {
background-position: -242px -2px;
}
/* ================================================================== */
/* Drawing styles
/* ================================================================== */
.leaflet-mouse-marker {
background-color: #fff;
cursor: crosshair;
}
.leaflet-draw-tooltip {
background: rgb(54, 54, 54);
background: rgba(0, 0, 0, 0.5);
border: 1px solid transparent;
-webkit-border-radius: 4px;
border-radius: 4px;
color: #fff;
font: 12px/18px "Helvetica Neue", Arial, Helvetica, sans-serif;
margin-left: 20px;
margin-top: -21px;
padding: 4px 8px;
position: absolute;
visibility: hidden;
white-space: nowrap;
z-index: 6;
}
.leaflet-draw-tooltip:before {
border-right: 6px solid black;
border-right-color: rgba(0, 0, 0, 0.5);
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
content: "";
position: absolute;
top: 7px;
left: -7px;
}
.leaflet-error-draw-tooltip {
background-color: #F2DEDE;
border: 1px solid #E6B6BD;
color: #B94A48;
}
.leaflet-error-draw-tooltip:before {
border-right-color: #E6B6BD;
}
.leaflet-draw-tooltip-single {
margin-top: -12px
}
.leaflet-draw-tooltip-subtext {
color: #f8d5e4;
}
.leaflet-draw-guide-dash {
font-size: 1%;
opacity: 0.6;
position: absolute;
width: 5px;
height: 5px;
}
/* ================================================================== */
/* Edit styles
/* ================================================================== */
.leaflet-edit-marker-selected {
background: rgba(254, 87, 161, 0.1);
border: 4px dashed rgba(254, 87, 161, 0.6);
-webkit-border-radius: 4px;
border-radius: 4px;
}
.leaflet-edit-move {
cursor: move;
}
.leaflet-edit-resize {
cursor: pointer;
}
/* ================================================================== */
/* Old IE styles
/* ================================================================== */
.leaflet-oldie .leaflet-draw-toolbar {
border: 3px solid #999;
}
.leaflet-oldie .leaflet-draw-toolbar a {
background-color: #eee;
}
.leaflet-oldie .leaflet-draw-toolbar a:hover {
background-color: #fff;
}
.leaflet-oldie .leaflet-draw-actions {
left: 32px;
margin-top: 3px;
}
.leaflet-oldie .leaflet-draw-actions li {
display: inline;
zoom: 1;
}
.leaflet-oldie .leaflet-edit-marker-selected {
border: 4px dashed #fe93c2;
}
.leaflet-oldie .leaflet-draw-actions a {
background-color: #999;
}
.leaflet-oldie .leaflet-draw-actions a:hover {
background-color: #a5a5a5;
}
.leaflet-oldie .leaflet-draw-actions-top a {
margin-top: 1px;
}
.leaflet-oldie .leaflet-draw-actions-bottom a {
height: 28px;
line-height: 28px;
}
.leaflet-oldie .leaflet-draw-actions-top.leaflet-draw-actions-bottom a {
height: 27px;
line-height: 27px;
}
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -174,9 +174,20 @@
{% macro form_css() %}
<link href="{{ admin_static.url(filename='vendor/select2/select2.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker-bs2.css') }}" rel="stylesheet">
{% if config.MAPBOX_MAP_ID %}
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet">
{% endif %}
{% endmacro %}
{% macro form_js() %}
{% if config.MAPBOX_MAP_ID %}
<script>
window.MAPBOX_MAP_ID = "{{ config.MAPBOX_MAP_ID }}";
</script>
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.js') }}"></script>
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/form.js') }}"></script>
{% endmacro %}
......@@ -167,9 +167,20 @@
{% macro form_css() %}
<link href="{{ admin_static.url(filename='vendor/select2/select2.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker-bs3.css') }}" rel="stylesheet">
{% if config.MAPBOX_MAP_ID %}
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.css') }}" rel="stylesheet">
<link href="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.css') }}" rel="stylesheet">
{% endif %}
{% endmacro %}
{% macro form_js() %}
{% if config.MAPBOX_MAP_ID %}
<script>
window.MAPBOX_MAP_ID = "{{ config.MAPBOX_MAP_ID }}";
</script>
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.js') }}"></script>
<script src="{{ admin_static.url(filename='vendor/leaflet/leaflet.draw.js') }}"></script>
{% endif %}
<script src="{{ admin_static.url(filename='vendor/bootstrap-daterangepicker/daterangepicker.js') }}"></script>
<script src="{{ admin_static.url(filename='admin/js/form.js') }}"></script>
{% endmacro %}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment