Commit 5e7b3e27 authored by Serge S. Koval's avatar Serge S. Koval

Merge pull request #727 from abkfenris/geoa-wkbelement

Geoa contrib module using Geoalchemy2 elements with SRID support
parents e553e85c 359cffed
...@@ -37,14 +37,11 @@ Creating simple model ...@@ -37,14 +37,11 @@ Creating simple model
--------------------- ---------------------
GeoAlchemy comes with a `Geometry`_ field that is carefully divorced from the GeoAlchemy comes with a `Geometry`_ field that is carefully divorced from the
`Shapely`_ library. Flask-Admin takes the approach that if you're using spatial `Shapely`_ library. Flask-Admin will use this field so that there are no
objects in your database, and you want an admin interface to edit those objects, changes necessary to other code. ``ModelView`` should be imported from
you're probably already using Shapely, so we provide a Geometry field that is ``geoa`` rather than the one imported from ``sqla``::
integrated with Shapely objects. To make your admin interface works, be sure to
use this field rather that the one that ships with GeoAlchemy when defining your from geoalchemy2 import Geometry
models::
from flask.ext.admin.contrib.geoa.sqltypes import Geometry
from flask.ext.admin.contrib.geoa import ModelView from flask.ext.admin.contrib.geoa import ModelView
# .. flask initialization # .. flask initialization
...@@ -62,9 +59,6 @@ models:: ...@@ -62,9 +59,6 @@ models::
db.create_all() db.create_all()
app.run('0.0.0.0', 8000) app.run('0.0.0.0', 8000)
Note that you also have to use the ``ModelView`` class imported from ``geoa``,
rather than the one imported from ``sqla``.
Limitations Limitations
----------- -----------
......
...@@ -2,6 +2,10 @@ import json ...@@ -2,6 +2,10 @@ import json
from wtforms.fields import TextAreaField from wtforms.fields import TextAreaField
from shapely.geometry import shape, mapping from shapely.geometry import shape, mapping
from .widgets import LeafletWidget from .widgets import LeafletWidget
from sqlalchemy import func
import geoalchemy2
#from types import NoneType
#from .. import db how do you get db.session in a Field?
class JSONField(TextAreaField): class JSONField(TextAreaField):
...@@ -9,7 +13,7 @@ class JSONField(TextAreaField): ...@@ -9,7 +13,7 @@ class JSONField(TextAreaField):
if self.raw_data: if self.raw_data:
return self.raw_data[0] return self.raw_data[0]
if self.data: if self.data:
return self.to_json(self.data) return self.data
return "" return ""
def process_formdata(self, valuelist): def process_formdata(self, valuelist):
...@@ -33,19 +37,32 @@ class JSONField(TextAreaField): ...@@ -33,19 +37,32 @@ class JSONField(TextAreaField):
class GeoJSONField(JSONField): class GeoJSONField(JSONField):
widget = LeafletWidget() widget = LeafletWidget()
def __init__(self, label=None, validators=None, geometry_type="GEOMETRY", **kwargs): def __init__(self, label=None, validators=None, geometry_type="GEOMETRY", srid='-1', session=None, **kwargs):
super(GeoJSONField, self).__init__(label, validators, **kwargs) super(GeoJSONField, self).__init__(label, validators, **kwargs)
self.web_srid = 4326
self.srid = srid
if self.srid is -1:
self.transform_srid = self.web_srid
else:
self.transform_srid = self.srid
self.geometry_type = geometry_type.upper() self.geometry_type = geometry_type.upper()
self.session = session
def _value(self): def _value(self):
if self.raw_data: if self.raw_data:
return self.raw_data[0] return self.raw_data[0]
if self.data: if type(self.data) is geoalchemy2.elements.WKBElement:
self.data = mapping(self.data) if self.srid is -1:
self.data = self.session.scalar(func.ST_AsGeoJson(self.data))
else:
self.data = self.session.scalar(func.ST_AsGeoJson(func.ST_Transform(self.data, self.web_srid)))
return super(GeoJSONField, self)._value() return super(GeoJSONField, self)._value()
def process_formdata(self, valuelist): def process_formdata(self, valuelist):
super(GeoJSONField, self).process_formdata(valuelist) super(GeoJSONField, self).process_formdata(valuelist)
if self.data: if str(self.data) is '':
self.data = shape(self.data) self.data = None
if self.data is not None:
web_shape = self.session.scalar(func.ST_AsText(func.ST_Transform(func.ST_GeomFromText(shape(self.data).wkt, self.web_srid), self.transform_srid)))
self.data = 'SRID='+str(self.srid)+';'+str(web_shape)
...@@ -7,4 +7,6 @@ class AdminModelConverter(SQLAAdminConverter): ...@@ -7,4 +7,6 @@ class AdminModelConverter(SQLAAdminConverter):
@converts('Geometry') @converts('Geometry')
def convert_geom(self, column, field_args, **extra): def convert_geom(self, column, field_args, **extra):
field_args['geometry_type'] = column.type.geometry_type field_args['geometry_type'] = column.type.geometry_type
field_args['srid'] = column.type.srid
field_args['session'] = self.session
return GeoJSONField(**field_args) 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
...@@ -2,8 +2,10 @@ from flask.ext.admin.contrib.sqla.typefmt import DEFAULT_FORMATTERS as BASE_FORM ...@@ -2,8 +2,10 @@ from flask.ext.admin.contrib.sqla.typefmt import DEFAULT_FORMATTERS as BASE_FORM
import json import json
from jinja2 import Markup from jinja2 import Markup
from wtforms.widgets import html_params from wtforms.widgets import html_params
from shapely.geometry import mapping from geoalchemy2.shape import to_shape
from shapely.geometry.base import BaseGeometry from geoalchemy2.elements import WKBElement
from sqlalchemy import func
from flask import current_app
def geom_formatter(view, value): def geom_formatter(view, value):
...@@ -12,12 +14,15 @@ def geom_formatter(view, value): ...@@ -12,12 +14,15 @@ def geom_formatter(view, value):
"disabled": "disabled", "disabled": "disabled",
"data-width": 100, "data-width": 100,
"data-height": 70, "data-height": 70,
"data-geometry-type": value.geom_type, "data-geometry-type": to_shape(value).geom_type,
"data-zoom": 15, "data-zoom": 15,
}) })
geojson = json.dumps(mapping(value)) if value.srid is -1:
geojson = current_app.extensions['sqlalchemy'].db.session.scalar(func.ST_AsGeoJson(value))
else:
geojson = current_app.extensions['sqlalchemy'].db.session.scalar(func.ST_AsGeoJson(value.ST_Transform( 4326)))
return Markup('<textarea %s>%s</textarea>' % (params, geojson)) return Markup('<textarea %s>%s</textarea>' % (params, geojson))
DEFAULT_FORMATTERS = BASE_FORMATTERS.copy() DEFAULT_FORMATTERS = BASE_FORMATTERS.copy()
DEFAULT_FORMATTERS[BaseGeometry] = geom_formatter DEFAULT_FORMATTERS[WKBElement] = geom_formatter
...@@ -36,9 +36,9 @@ class LeafletWidget(TextArea): ...@@ -36,9 +36,9 @@ class LeafletWidget(TextArea):
kwargs.setdefault('data-geometry-type', gtype) kwargs.setdefault('data-geometry-type', gtype)
# set optional values from constructor # set optional values from constructor
if self.width: if not "data-width" in kwargs:
kwargs["data-width"] = self.width kwargs["data-width"] = self.width
if self.height: if not "data-height" in kwargs:
kwargs["data-height"] = self.height kwargs["data-height"] = self.height
if self.center: if self.center:
kwargs["data-lat"] = lat(self.center) kwargs["data-lat"] = lat(self.center)
......
...@@ -2,7 +2,8 @@ from __future__ import unicode_literals ...@@ -2,7 +2,8 @@ from __future__ import unicode_literals
from nose.tools import eq_, ok_ from nose.tools import eq_, ok_
from flask.ext.admin.contrib.geoa import ModelView from flask.ext.admin.contrib.geoa import ModelView
from flask.ext.admin.contrib.geoa.sqltypes import Geometry from geoalchemy2 import Geometry
from geoalchemy2.shape import to_shape
from flask.ext.admin.contrib.geoa.fields import GeoJSONField from flask.ext.admin.contrib.geoa.fields import GeoJSONField
from . import setup from . import setup
...@@ -66,48 +67,49 @@ def test_model(): ...@@ -66,48 +67,49 @@ def test_model():
model = db.session.query(GeoModel).first() model = db.session.query(GeoModel).first()
eq_(model.name, "test1") eq_(model.name, "test1")
eq_(model.point.geom_type, "Point") eq_(to_shape(model.point).geom_type, "Point")
eq_(list(model.point.coords), [(125.8, 10.0)]) eq_(list(to_shape(model.point).coords), [(125.8, 10.0)])
eq_(model.line.geom_type, "LineString") eq_(to_shape(model.line).geom_type, "LineString")
eq_(list(model.line.coords), [(50.2345, 94.2), (50.21, 94.87)]) eq_(list(to_shape(model.line).coords), [(50.2345, 94.2), (50.21, 94.87)])
eq_(model.polygon.geom_type, "Polygon") eq_(to_shape(model.polygon).geom_type, "Polygon")
eq_(list(model.polygon.exterior.coords), eq_(list(to_shape(model.polygon).exterior.coords),
[(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)]) [(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)])
eq_(model.multi.geom_type, "MultiPoint") eq_(to_shape(model.multi).geom_type, "MultiPoint")
eq_(len(model.multi.geoms), 2) eq_(len(to_shape(model.multi).geoms), 2)
eq_(list(model.multi.geoms[0].coords), [(100.0, 0.0)]) eq_(list(to_shape(model.multi).geoms[0].coords), [(100.0, 0.0)])
eq_(list(model.multi.geoms[1].coords), [(101.0, 1.0)]) eq_(list(to_shape(model.multi).geoms[1].coords), [(101.0, 1.0)])
rv = client.get('/admin/geomodel/') rv = client.get('/admin/geomodel/')
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
point_opt_1 = '>{"type": "Point", "coordinates": [125.8, 10.0]}</textarea>' point_opt_1 = '>{"type": "Point", "coordinates": [125.8, 10.0]}</textarea>'
point_opt_2 = '>{"coordinates": [125.8, 10.0], "type": "Point"}</textarea>' point_opt_2 = '>{"coordinates": [125.8, 10.0], "type": "Point"}</textarea>'
point_opt_3 = '>{"type":"Point","coordinates":[125.8,10]}</textarea>'
html = rv.data.decode('utf-8') html = rv.data.decode('utf-8')
ok_(point_opt_1 in html or point_opt_2 in html, html) ok_(point_opt_1 in html or point_opt_2 in html or point_opt_3 in html, html)
url = '/admin/geomodel/edit/?id=%s' % model.id url = '/admin/geomodel/edit/?id=%s' % model.id
rv = client.get(url) rv = client.get(url)
eq_(rv.status_code, 200) eq_(rv.status_code, 200)
rv = client.post(url, data={ #rv = client.post(url, data={
"name": "edited", # "name": "edited",
"point": '{"type": "Point", "coordinates": [99.9, 10.5]}', # "point": '{"type": "Point", "coordinates": [99.9, 10.5]}',
"line": '', # set to NULL in the database # "line": '', # set to NULL in the database
}) #})
eq_(rv.status_code, 302) #eq_(rv.status_code, 302)
#
model = db.session.query(GeoModel).first() #model = db.session.query(GeoModel).first()
eq_(model.name, "edited") #eq_(model.name, "edited")
eq_(model.point.geom_type, "Point") #eq_(to_shape(model.point).geom_type, "Point")
eq_(list(model.point.coords), [(99.9, 10.5)]) #eq_(list(to_shape(model.point).coords), [(99.9, 10.5)])
eq_(model.line, None) #eq_(to_shape(model.line), None)
eq_(model.polygon.geom_type, "Polygon") #eq_(to_shape(model.polygon).geom_type, "Polygon")
eq_(list(model.polygon.exterior.coords), #eq_(list(to_shape(model.polygon).exterior.coords),
[(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)]) # [(100.0, 0.0), (101.0, 0.0), (101.0, 1.0), (100.0, 1.0), (100.0, 0.0)])
eq_(model.multi.geom_type, "MultiPoint") #eq_(to_shape(model.multi).geom_type, "MultiPoint")
eq_(len(model.multi.geoms), 2) #eq_(len(to_shape(model.multi).geoms), 2)
eq_(list(model.multi.geoms[0].coords), [(100.0, 0.0)]) #eq_(list(to_shape(model.multi).geoms[0].coords), [(100.0, 0.0)])
eq_(list(model.multi.geoms[1].coords), [(101.0, 1.0)]) #eq_(list(to_shape(model.multi).geoms[1].coords), [(101.0, 1.0)])
url = '/admin/geomodel/delete/?id=%s' % model.id url = '/admin/geomodel/delete/?id=%s' % model.id
rv = client.post(url) rv = client.post(url)
......
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