Commit bc0f282f authored by Paul Brown's avatar Paul Brown

Merge pull request #1231 from jmagnusson/mapbox-flip-coordinates

Allow flipping mapbox coordinates to/from db
parents 02084e12 86d2542b
import json import json
import warnings
import geoalchemy2
from flask import current_app
from shapely.geometry import shape
from sqlalchemy import func
from wtforms.fields import TextAreaField from wtforms.fields import TextAreaField
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):
...@@ -37,32 +39,69 @@ class JSONField(TextAreaField): ...@@ -37,32 +39,69 @@ class JSONField(TextAreaField):
class GeoJSONField(JSONField): class GeoJSONField(JSONField):
widget = LeafletWidget() widget = LeafletWidget()
def __init__(self, label=None, validators=None, geometry_type="GEOMETRY", srid='-1', session=None, **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.web_srid = 4326
self.srid = srid self.srid = srid
if self.srid is -1: if self.srid is -1:
self.transform_srid = self.web_srid self.transform_srid = self.web_srid
else: else:
self.transform_srid = self.srid self.transform_srid = self.srid
self.geometry_type = geometry_type.upper() self.geometry_type = geometry_type.upper()
self.session = session self.session = session
def _flip_coordinates(self, other_func):
if current_app.config.get('MAPBOX_FIX_COORDINATES_ORDER'):
return func.ST_FlipCoordinates(other_func)
else:
warnings.warn(
'Consider setting the Flask config option '
'MAPBOX_FIX_COORDINATES_ORDER as the current implementation '
'passes lng/lat coordinates in the wrong order to '
'Leaflet. Without this setting any coordinates saved will '
'have flipped coordinates in your database. '
'Please note that this will become the standard behavior in '
'the next major version of Flask-Admin.'
)
return other_func
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 type(self.data) is geoalchemy2.elements.WKBElement: if type(self.data) is geoalchemy2.elements.WKBElement:
if self.srid is -1: if self.srid is -1:
self.data = self.session.scalar(func.ST_AsGeoJson(self.data)) self.data = self.session.scalar(
func.ST_AsGeoJson(
self._flip_coordinates(self.data)
)
)
else: else:
self.data = self.session.scalar(func.ST_AsGeoJson(func.ST_Transform(self.data, self.web_srid))) self.data = self.session.scalar(
func.ST_AsGeoJson(
self._flip_coordinates(
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 str(self.data) is '': if str(self.data) is '':
self.data = None self.data = None
if self.data is not 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))) web_shape = self.session.scalar(
self.data = 'SRID='+str(self.srid)+';'+str(web_shape) func.ST_AsText(
self._flip_coordinates(
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)
from __future__ import unicode_literals from __future__ import unicode_literals
from nose.tools import eq_, ok_ import json
import re
from flask_admin.contrib.geoa import ModelView from flask_admin.contrib.geoa import ModelView
from flask_admin.contrib.geoa.fields import GeoJSONField
from geoalchemy2 import Geometry from geoalchemy2 import Geometry
from geoalchemy2.shape import to_shape from geoalchemy2.shape import to_shape
from flask_admin.contrib.geoa.fields import GeoJSONField from nose.tools import eq_
from . import setup from . import setup
...@@ -30,6 +32,8 @@ def test_model(): ...@@ -30,6 +32,8 @@ def test_model():
app, db, admin = setup() app, db, admin = setup()
GeoModel = create_models(db) GeoModel = create_models(db)
db.create_all() db.create_all()
GeoModel.query.delete()
db.session.commit()
view = ModelView(GeoModel, db.session) view = ModelView(GeoModel, db.session)
admin.add_view(view) admin.add_view(view)
...@@ -81,37 +85,84 @@ def test_model(): ...@@ -81,37 +85,84 @@ def test_model():
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_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 or point_opt_3 in html, html) pattern = r'(.|\n)+({.*"type": ?"Point".*})</textarea>(.|\n)+'
group = re.match(pattern, html).group(2)
p = json.loads(group)
eq_(p['coordinates'][0], 125.8)
eq_(p['coordinates'][1], 10.0)
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_(to_shape(model.point).geom_type, "Point") # eq_(to_shape(model.point).geom_type, "Point")
#eq_(list(to_shape(model.point).coords), [(99.9, 10.5)]) # eq_(list(to_shape(model.point).coords), [(99.9, 10.5)])
#eq_(to_shape(model.line), None) # eq_(to_shape(model.line), None)
#eq_(to_shape(model.polygon).geom_type, "Polygon") # eq_(to_shape(model.polygon).geom_type, "Polygon")
#eq_(list(to_shape(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_(to_shape(model.multi).geom_type, "MultiPoint") # eq_(to_shape(model.multi).geom_type, "MultiPoint")
#eq_(len(to_shape(model.multi).geoms), 2) # eq_(len(to_shape(model.multi).geoms), 2)
#eq_(list(to_shape(model.multi).geoms[0].coords), [(100.0, 0.0)]) # eq_(list(to_shape(model.multi).geoms[0].coords), [(100.0, 0.0)])
#eq_(list(to_shape(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)
eq_(rv.status_code, 302) eq_(rv.status_code, 302)
eq_(db.session.query(GeoModel).count(), 0) eq_(db.session.query(GeoModel).count(), 0)
def test_mapbox_fix_point_coordinates():
app, db, admin = setup()
app.config['MAPBOX_FIX_COORDINATES_ORDER'] = True
GeoModel = create_models(db)
db.create_all()
GeoModel.query.delete()
db.session.commit()
view = ModelView(GeoModel, db.session)
admin.add_view(view)
# Make some test clients
client = app.test_client()
rv = client.post('/admin/geomodel/new/', data={
"name": "test1",
"point": '{"type": "Point", "coordinates": [125.8, 10.0]}',
"line": '{"type": "LineString", "coordinates": [[50.2345, 94.2], [50.21, 94.87]]}',
"polygon": '{"type": "Polygon", "coordinates": [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]}',
"multi": '{"type": "MultiPoint", "coordinates": [[100.0, 0.0], [101.0, 1.0]]}',
})
model = db.session.query(GeoModel).first()
# Notice how the coordinates are reversed here, i.e. longitude first which
# is the way it's stored in PostGIS columns.
eq_(list(to_shape(model.point).coords), [(10.0, 125.8)])
eq_(list(to_shape(model.line).coords), [(94.2, 50.2345), (94.87, 50.21)])
eq_(list(to_shape(model.polygon).exterior.coords),
[(0.0, 100.0), (0.0, 101.0), (1.0, 101.0), (1.0, 100.0), (0.0, 100.0)])
eq_(list(to_shape(model.multi).geoms[0].coords), [(0.0, 100.0)])
eq_(list(to_shape(model.multi).geoms[1].coords), [(1.0, 101.0)])
rv = client.get('/admin/geomodel/')
eq_(rv.status_code, 200)
html = rv.data.decode('utf-8')
pattern = r'(.|\n)+({.*"type": ?"Point".*})</textarea>(.|\n)+'
group = re.match(pattern, html).group(2)
p = json.loads(group)
# Reversed order again, so that it's parsed correctly by leaflet
eq_(p['coordinates'][0], 10.0)
eq_(p['coordinates'][1], 125.8)
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