Repository: flask-admin/flask-admin

Issue:
Wrong assumptions behind MAPBOX_FIX_COORDINATES_ORDER
@pawl @jmagnusson 

I just faced a MAPBOX_FIX_COORDINATES_ORDER warning, introduced by #1231. And there is a problem with it.

> current Leaflet implementation expects coordinates in latitude-longitude order but GeoJSON

Waaat?.. Leaflet don't know how to deal with GeoJSON? Seriously? Any sources / links / docs / confirmations? I'm absolutely sure that someone just has wrong coordinate order in his data. With current implementation (with MAPBOX_FIX_COORDINATES_ORDER=True) I getting wrong coordinate values in database:

```
[ ei-grad@ei-grad ~ ]
→ psql -t discharges -c 'select st_asgeojson(geom) from campaign_geo_limits'
 {"type":"Polygon","coordinates":[[[55.7462457665516,37.5826299190521],[55.7636337005308,37.5884664058685],[55.7715523006922,37.5997960567474],[55.7738696355073,37.626918554306],[55.7690416991694,37.6492345333099],[55.7632473862086,37.6554143428802],[55.7431537667293,37.6550710201263],[55.7319432132944,37.6406514644623],[55.7302033560511,37.6121556758881],[55.7383220264735,37.5877797603607],[55.7462457665516,37.5826299190521]]]}
```

I just draw this polygon near Moscow in flask-admin edit view. Moscow lat/lon are 55/37, so the coordinate order stored in database is wrong.

Btw, is it really worth to execute a spatial database query to flip two numbers?

I suggest to remove all that wrong coordinate flipping logic and handle somehow coordinate projection without that database jerking, maybe using `pyproj` or bring the proper projection handling to leaflet (it is able to deal with projections too, but I'd prefer to stick with python instead of javascript in flask application). PR will follow.



Discussion:
Started work in https://github.com/ei-grad/flask-admin/tree/geoa_refactoring, it already works, but I'm not sure if it should be so compatibility breaking.

~~I felt pretty bad when I sent in the PR about introducing the fix as an opt-in, just wanted a fix for it right there and then. I would love to see a proper clean fix for this instead, keep up the good work!~~

EDIT: I think actually my assumptions have been wrong, my source data and own stupidity is probably to blame. Fortunately the setting that I introduced was opt-in, so probably not too many people were using it.

@ei-grad How'd it go with your refactor? I'm thinking to do a revert PR for now... The warning is pretty annoying!

Sorry, I'm a bit busy with things far from flask-admin during last weeks, but I'm pretty sure I'll be able to redo my refactor soon, keeping the GeoJSON format in place (in current version GeoJSON is replaced with WKT, which is used by geoalchemy2, because it looked good and quick fix for me, and I was needed to launch a service quickly with it, but it looks like WKT transformation in browser is a kind of slow operation... though this fact needs more research, but still it is not a good idea to change format in forms, because I know at least one man who is relying on it to be GeoJSON in default flask-admin's-generated forms).
I hope I will come with pull-request this weekend.

Btw, revert it is not a bad idea, I'd rebase with it after revert, it is not a problem.


Resolving patch:
diff --git a/flask_admin/contrib/geoa/fields.py b/flask_admin/contrib/geoa/fields.py
--- a/flask_admin/contrib/geoa/fields.py
+++ b/flask_admin/contrib/geoa/fields.py
@@ -1,7 +1,4 @@
-import warnings
-
 import geoalchemy2
-from flask import current_app
 from shapely.geometry import shape
 from sqlalchemy import func
 
@@ -25,37 +22,16 @@ def __init__(self, label=None, validators=None, geometry_type="GEOMETRY",
         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:
-                return self.session.scalar(
-                    func.ST_AsGeoJson(
-                        self._flip_coordinates(self.data)
-                    )
-                )
+                return self.session.scalar(func.ST_AsGeoJson(self.data))
             else:
                 return self.session.scalar(
                     func.ST_AsGeoJson(
-                        self._flip_coordinates(
-                            func.ST_Transform(self.data, self.web_srid)
-                        )
+                        func.ST_Transform(self.data, self.web_srid)
                     )
                 )
         else:
@@ -68,14 +44,12 @@ def process_formdata(self, valuelist):
         if self.data is not None:
             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
-                        )
+                    func.ST_Transform(
+                        func.ST_GeomFromText(
+                            shape(self.data).wkt,
+                            self.web_srid
+                        ),
+                        self.transform_srid
                     )
                 )
             )


Test patch:
diff --git a/flask_admin/tests/geoa/test_basic.py b/flask_admin/tests/geoa/test_basic.py
--- a/flask_admin/tests/geoa/test_basic.py
+++ b/flask_admin/tests/geoa/test_basic.py
@@ -125,51 +125,6 @@ def test_model():
     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)
-
-
 def test_none():
     app, db, admin = setup()
     GeoModel = create_models(db)