Search code examples
pythonsqlalchemyflaskflask-admingeoalchemy

AdminModelConvertor for Geometry Field (LON/LAT)


I want to create a view for Flask-Admin to enter coordinates in a Geometry Field. How can I create two Textfields and convert them into the Geometry Object?

This is what I have tried so far (Besides uncountable other things)

class CustomAdminConverter(AdminModelConverter):
    @converts('geoalchemy2.types.Geometry')
    def convert_geometry(self, field_args, **extra):
        return WayToCoordinatesField(**field_args)

class WayToCoordinatesField(wtf.TextAreaField):
    def process_data(self, value):
        print "run" #is never called??
        if value is None:
            value = {}
        else:
            value = "test"
        return value

class POIView(ModelView):
    inline_model_form_converter = MyInlineModelConverter
    model_form_converter=CustomAdminConverter
    can_create = True
    def __init__(self, session, **kwargs):
        # You can pass name and other parameters if you want to
        super(POIView, self).__init__(POI, session, **kwargs)

    def scaffold_form(self):
        form_class = super(POIView, self).scaffold_form()
        form_class.way = wtf.TextAreaField("Coordinates")
        return form_class

The POI Object looks like this:

class POI(db.Model):
    __tablename__ = 'zo_poi'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Text())
    tags = db.Column(HSTORE())
    src = db.Column(db.Text())
    way = db.Column(Geometry('POINT'))
    intern = db.Column(db.BOOLEAN())

Thanks a lot for your help!


Solution

  • Got a solution with a interactive Map. Here is what I have done:

    admin/fields.py:

    import json
    from wtforms import Field
    import geojson
    from shapely.geometry import asShape
    from geoalchemy2.shape import to_shape, from_shape
    from wtforms.widgets import html_params, HTMLString
    from geoalchemy2.elements import WKTElement, WKBElement
    from flask import render_template
    class WTFormsMapInput(object):
        def __call__(self, field, **kwargs):
            options = dict(name=field.name, value=field.data, height=field.height, width=field.width,
                           geometry_type=field.geometry_type)
    
            return HTMLString(render_template("admin/admin_map.html", height=options['height'], width=options['width'],
                                              geolayer=self.geolayer(field.data), preview=False))
    
        def geolayer(self, value):
            if value is not None:
                html = ""
                subme = """var geojson = JSON.parse('%s');
                           editableLayers.addData(geojson);
                           update()
                           map.fitBounds(editableLayers.getBounds());"""
                # If validation in Flask-Admin fails on somethign other than
                # the spatial column, it is never converted to geojson.  Didn't
                # spend the time to figure out why, so I just convert here.
                if isinstance(value, (WKTElement, WKBElement)):
                    html += subme % geojson.dumps(to_shape(value))
                else:
                    html += subme % geojson.dumps(value)
                return html
    
    
    class WTFormsMapField(Field):
        widget = WTFormsMapInput()
    
        def __init__(self, label='', validators=None, geometry_type=None, width=500, height=500,
                     **kwargs):
            super(WTFormsMapField, self).__init__(label, validators, **kwargs)
            self.width = width
            self.height = height
            self.geometry_type = geometry_type
    
        def _value(self):
            """ Called by widget to get GeoJSON representation of object """
            if self.data:
                return self.data
            else:
                return json.loads(json.dumps(dict()))
    
        def process_formdata(self, valuelist):
            """ Convert GeoJSON to DB object """
            if valuelist:
                geo_ob = geojson.loads(valuelist[0])
                self.data = from_shape(asShape(geo_ob.geometry))
            else:
                self.data = None
    
        def process_data(self, value):
            """ Convert DB object to GeoJSON """
            if value is not None:
                self.data = geojson.loads(geojson.dumps(to_shape(value)))
                print self.data
            else:
                self.data = None
    

    templates/admin/admin_map.html

    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.css"/>
    <link rel="stylesheet" href="http://leaflet.github.io/Leaflet.draw/leaflet.draw.css"/>
    <script src="http://cdn.leafletjs.com/leaflet-0.7.2/leaflet.js"></script>
    <script src="http://leaflet.github.io/Leaflet.draw/leaflet.draw.js"></script>
    <script src="/admin/static/vendor/jquery-1.8.3.min.js" type="text/javascript"></script>
    <script src="/static/js/googleOverlay/layer/tile/Google.js"></script>
    <script src="http://maps.google.com/maps/api/js?v=3&sensor=false"></script>
    
    <div id="map" style="height: {{ height }}px; width: {{ width }}px;"></div>
    <input id="geojson" type="text" name="{{ name }}"/>
    
    <script>
        var map = new L.Map('map', {
                    center: new L.LatLng(47.3682, 8.879),
                    zoom: 11
                    {%  if preview %}
                    ,
                        dragging: false,
                        touchzoom: false,
                        scrollWheelZoom: false,
                        doubleClickZoom: false,
                        boxZoom: false,
                        tap: false,
                        keyboard: false,
                        zoomControl: false
    
                    {% endif %}
                }
        );
        var ggl = new L.Google('ROADMAP');
        map.addLayer(ggl);
        var osm = new L.TileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png');
        map.addControl(new L.Control.Layers({'OpenStreetMap': osm, 'Google Maps': ggl}, {}));
    
        var editableLayers = L.geoJson().addTo(map);
    
        {{ geolayer |safe }}
        {% if not preview %}
        var drawControl = new L.Control.Draw({
            position: 'topright',
            draw: {
                polyline: false,
                circle: false,
                rectangle: false,
                polygon: true,
                marker: true,
            },
            edit: {
                featureGroup: editableLayers
            }
        });
        {% endif %}
        map.addControl(drawControl);
    
        map.on('draw:created', function (e) {
            editableLayers.addLayer(e.layer);
            update();
        });
    
        map.on('draw:edited', function (e) {
            // Just use the first layer
            update();
        })
    
        map.on('draw:deleted', function (e) {
            update();
        })
    
        function update() {
            if (editableLayers.getLayers().length > 0) {
                $("#geojson").val(JSON.stringify(editableLayers.getLayers()[0].toGeoJSON()));
            } else {
                $("#geojson").val(null);
            }
        }
    
    </script>
    

    admin/views.py

    class POIView(ModelView):
        can_create = True
        form_overrides = dict(location=WTFormsMapField)
        form_args = dict(
            way=dict(
                geometry_type='Polygon', height=500, width=500
            )
        )
        column_formatters = dict(tags=lambda v, c, m, p: (u', '.join(u"=".join([k, v]) for k, v in m.tags.items())),
                                 )
    
        def __init__(self, session, **kwargs):
            super(POIView, self).__init__(POI, session, **kwargs)
    
        def scaffold_form(self):
            form_class = super(POIView, self).scaffold_form()
            form_class.way = WTFormsMapField()
            form_class.tags = MySelect2TagsField("Tags",None)
            return form_class
    

    admin/models.py

    class POI(db.Model):
        __tablename__ = 'zo_poi'
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.Text())
        tags = db.Column(HSTORE())
        src = db.Column(db.Text())
        way = db.Column(Geometry('point', srid=4326))
        intern = db.Column(db.BOOLEAN())