3

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!

Remo L.
  • 720
  • 6
  • 19

2 Answers2

3

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())
Remo L.
  • 720
  • 6
  • 19
2

As of version 1.0.9 of Flask-Admin, it now supports Geoalchemy2 Geometry columns (and 1.1.0 added Geography columns).

The biggest change is to import ModelView from flask-admin.contrib.geoa instead of flask-admin.contrib.sqla, so a simple model will look like:

from geoalchemy2 import Geometry
from flask-admin.contrib.geoa import ModelView

app.config['MAPBOX_MAP_ID'] = 'example.abc123'    

class Location(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    point = db.Column(Geometry("Point", 4326))

admin = Admin(app)
admin.add_view(ModelView(Location, db.session))

In the list view, a small preview map will be shown with each, and then a leaflet.draw view in edit or create views.

For a line or complicated polygon you may want to override ModelView with form_widget_args to get a more reasonably sized editing space.

class Polygon(db.Model):
    ...
    polygon = db.Column(Geometry("Polygon", 4326)

class PolygonView(ModelView):
    form_widget_args = {'polgon': {'data-height': 400; 'data-width': 400}}

admin.add_view(PolygonView(Polygon, db.session))
Alex Kerney
  • 121
  • 1
  • 2
  • wow.. ok will definitively check this out when I'm working on that project again :) thx. – Remo L. Mar 16 '15 at 07:02
  • In the current version of Flask-Admin(1.1.0), the support for Geometry columns is broken since Mapbox API upgraded to v4. https://github.com/mrjoes/flask-admin/blob/ac5fe084fb9803d791d6c6441af4c456b9eeaf3b/flask_admin/static/admin/js/form-1.0.0.js#L158 – blurrcat Mar 24 '15 at 08:22
  • 1
    @blurrcat There is no support for the v3 API with newly created maps, but support hasn't been turned off for maps created before March 15, 2015. It is on my todo list to get v4 API support working. – Alex Kerney Mar 24 '15 at 11:09