Commit 86d2542b authored by Jacob Magnusson's avatar Jacob Magnusson

Allow flipping mapbox coordinates to/from db

The current Leaflet implementation expects coordinates in latitude-longitude order but GeoJSON and PostGIS works the other way around with lng first. These changes work around this behavior by passing the data to and from db through PostGIS’s `ST_FlipCoordinates` function if the setting `MAPBOX_FIX_POINT_COORDINATES` is something truthy. This is to not break the current behavior. Right now a warning is emitted when this setting hasn’t been enabled but I would recommend to remove this setting and make its behavior the default in the next major version.
parent c8fdf740
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 shapely.geometry import shape, mapping
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):
......@@ -37,32 +39,69 @@ class JSONField(TextAreaField):
class GeoJSONField(JSONField):
widget = LeafletWidget()
def __init__(self, label=None, validators=None, geometry_type="GEOMETRY", srid='-1', session=None, **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
self.transform_srid = self.web_srid
else:
self.transform_srid = self.srid
self.transform_srid = self.srid
self.geometry_type = geometry_type.upper()
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):
if self.raw_data:
return self.raw_data[0]
if type(self.data) is geoalchemy2.elements.WKBElement:
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:
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()
def process_formdata(self, valuelist):
super(GeoJSONField, self).process_formdata(valuelist)
if str(self.data) is '':
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)
web_shape = self.session.scalar(
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 nose.tools import eq_, ok_
import json
import re
from flask_admin.contrib.geoa import ModelView
from flask_admin.contrib.geoa.fields import GeoJSONField
from geoalchemy2 import Geometry
from geoalchemy2.shape import to_shape
from flask_admin.contrib.geoa.fields import GeoJSONField
from nose.tools import eq_
from . import setup
......@@ -30,6 +32,8 @@ def test_model():
app, db, admin = setup()
GeoModel = create_models(db)
db.create_all()
GeoModel.query.delete()
db.session.commit()
view = ModelView(GeoModel, db.session)
admin.add_view(view)
......@@ -81,37 +85,84 @@ def test_model():
rv = client.get('/admin/geomodel/')
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')
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
rv = client.get(url)
eq_(rv.status_code, 200)
#rv = client.post(url, data={
# "name": "edited",
# "point": '{"type": "Point", "coordinates": [99.9, 10.5]}',
# "line": '', # set to NULL in the database
#})
#eq_(rv.status_code, 302)
# rv = client.post(url, data={
# "name": "edited",
# "point": '{"type": "Point", "coordinates": [99.9, 10.5]}',
# "line": '', # set to NULL in the database
# })
# eq_(rv.status_code, 302)
#
#model = db.session.query(GeoModel).first()
#eq_(model.name, "edited")
#eq_(to_shape(model.point).geom_type, "Point")
#eq_(list(to_shape(model.point).coords), [(99.9, 10.5)])
#eq_(to_shape(model.line), None)
#eq_(to_shape(model.polygon).geom_type, "Polygon")
#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)])
#eq_(to_shape(model.multi).geom_type, "MultiPoint")
#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[1].coords), [(101.0, 1.0)])
# model = db.session.query(GeoModel).first()
# eq_(model.name, "edited")
# eq_(to_shape(model.point).geom_type, "Point")
# eq_(list(to_shape(model.point).coords), [(99.9, 10.5)])
# eq_(to_shape(model.line), None)
# eq_(to_shape(model.polygon).geom_type, "Polygon")
# 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)])
# eq_(to_shape(model.multi).geom_type, "MultiPoint")
# 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[1].coords), [(101.0, 1.0)])
url = '/admin/geomodel/delete/?id=%s' % model.id
rv = client.post(url)
eq_(rv.status_code, 302)
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