olwidget templates...

If you are using olwidget to get your geodjango editing up-and-running fast, and you want your users to be able to enter the GPS coordinates textually, as well as via the map, then yay, I've already gone through the pain for you.  Here is a hacky way to get synchronized text-field and map entry using an olwidget template.  Note: there are few bad practices here; you should see this as a quick hack to demonstrate that you can make such modifications, not that it's the right way to do it.

<h2 class="box">GPS Coordinates <input id="{{id}}_lat" name="lat" /> <input id="{{id}}_long" name="long" /></h2>
<div id="{{ id }}"></div>
{{ layer_html|join:"" }}
<script type="text/javascript">
var loadHandler = function( handler ) {
    if (document.addEventListener) {
        document.addEventListener("DOMContentLoaded", handler);
    } else {
        /* note: only supports IE8+ */
        document.onreadystatechange = function( ) {
            if (document.readyState == "complete") {
                handler();
            }
        }
    }
};
loadHandler( function() {

Here we're creating the text-editing widgets and defining a function that defers running of the code until after all scripts have been loaded.  That allows us to use any other scripts we're loading later in the document (in this example, that's just jquery, though for no other reason than it being convenient for me).

    {{ setup_custom_layer_types|safe }}
    var mapwidget = new olwidget.Map("{{ id }}", [
            {{ layer_js|join:"," }}
        ], 
        {{ map_opts|safe }}
    );
    var llong = $("#{{id}}_lat");
    var llat = $("#{{id}}_long");

The first part there is the admin widget template, just rendering in the custom layer definitions and creating the mapwidget.  We're namespaced inside the function here, so we give the widget the same name in all instances (mapwidget).  Then we get the llong and llat widgets for later usage.

    var setCenter = function(geom) {
        // zoomToDataExtent == false, or there is no data on any layer
        var center = new OpenLayers.LonLat(geom.geometry.x,geom.geometry.y);
        center = center.transform(mapwidget.displayProjection, mapwidget.projection);
        mapwidget.setCenter(center, mapwidget.zoom);
    };
    var layer = mapwidget.vectorLayers[0];

The function takes (point) geometry and recenters the map on the point with the same zoom.  The layer is the single editable layer we are using to specify the point.

Don't look at this next bit. It's monkey-patching the layer.  The correct way to do this would be to create a new olwidget.EditableLayer sub-class, get that definition added to media on a widget sub-class, and then use a custom template for the widget to instantiate that sub-class.  That's rather a lot more work than the couple of lines of monkey patching. I could register events on the OpenLayers events, but then I'd be duplicating the login in the olwidget code for figuring out what feature to process... so... don't do this.

    layer.oldFeatureToTextarea = layer.featureToTextarea;
    layer.featureToLatLong = function( feature ) {
        feature = olwidget.transformVector(feature,
                mapwidget.projection, mapwidget.displayProjection);
        llong.val( ''+feature.geometry.x );
        llat.val( ''+feature.geometry.y );
    };
    layer.featureToTextarea = function( feature ) {
        layer.oldFeatureToTextarea( feature );
        layer.featureToLatLong( feature );
    };

Note the transformation; this takes the coordinate from map-display to the GPS coordinates we are using.

    var onTypedValue = function() {
        var typedValue = 'SRID=4326;POINT ('+llong.val()+' '+llat.val()+')' ;
        try {
            var geom = olwidget.ewktToFeature(typedValue);
            if (isNaN(geom.geometry.x) || isNaN(geom.geometry.y)) {
                throw 'Non-numeric coordinates';
            }
            llong.removeClass( 'error' );
            llat.removeClass( 'error' );
        } catch (e) {
            console.log( 'Unable to parse: '+typedValue );
            llong.addClass( 'error' );
            llat.addClass( 'error' );
            return false;
        }
        layer.textarea.value = typedValue;
        layer.readWKT();
        setCenter(geom);
    };

This function just reads the current values from the text-editing widgets and sees if they form a valid coordinate, if they do, then it updates the underlying text area, tells the layer to pull its coordinates from the text area, and then pans the map to the new coordinate.

    llong.change( onTypedValue );
    llat.change( onTypedValue );
    try {
        var feature = olwidget.ewktToFeature( layer.textarea.value );
        llong.val( feature.geometry.x );
        llat.val( feature.geometry.y );
    } catch (e) {
        console.log( 'No feature found on load, not setting text area values' );
    }
});
</script>

We register the handler, and during initialization we set the widget values from the text area value (note that there is no transformation involved here). All that's left is to specify that you want to use this template for your MapModelForm subclass.

class MyForm( MapModelForm ):
    class Meta:
        template = 'olwidget/point_editor.html'

Comments

Comments are closed.

Pingbacks

Pingbacks are closed.

Trackbacks