olwidget show features in the window

So now you've got your spiffy textual-entry-of-GPS-coordinates-and-map admin GUI and you're wanting to actually let people see your "features" on a map.  olwidget has an InfoMap() which is basically a Map + a single InfoLayer, but it uses a static set of "info" rather than a dynamically updating set. Here's how to hack the dynamic updates in using jquery and simple ajax.

<div id="{{ id }}" data-url="{% url 'server_features' %}"></div>
{{ layer_html|join:"" }}
<script type="text/javascript">
var loadHandler = function( handler ) {
loadHandler( function() {
    {{ setup_custom_layer_types|safe }}
    var mapwidget = new olwidget.Map("{{ id }}", [
            {{ layer_js|join:"," }}
        {{ map_opts|safe }}
    var layer = mapwidget.vectorLayers[0];
    var map_div = $(mapwidget.div);

Pretty much the same setup as we saw for the GPS entry custom template, so we won't spend any time reviewing it save to point out the 'data-url' attribute on the div, which is where we'll get the ajax data.

    var replace_features = function( new_features ) {
        layer.info = new_features;
        var features = [];
        for (var i = 0; i < new_features.length; i++) {
            var feature = olwidget.ewktToFeature(new_features[i][0]);
            if (olwidget.isCollectionEmpty(feature)) {
            feature = olwidget.transformVector(feature,
                mapwidget.displayProjection, mapwidget.projection);
            if (feature.constructor != Array) {
                feature = [feature];
            var htmlInfo = new_features[i][1];
            for (var k = 0; k < feature.length; k++) {
                if (typeof htmlInfo === "object") {
                    feature[k].attributes = htmlInfo;
                    if (typeof htmlInfo.style !== "undefined") {
                        feature[k].style = OpenLayers.Util.applyDefaults(
                            htmlInfo.style, layer.opts.overlayStyle
                } else {
                    feature[k].attributes = { html: htmlInfo };

This function takes a set of feature definitions, in exactly the same format as the InfoLayer accepts.  It duplicates the code inside the olwidget's InfoLayer.afterAdd because that method is also calling the superclass's implementation.  The only new lines are the first two, which clear out any current features and store "info" locally.  Now we'll actually hook up that function:

    layer.events.register( 'moveend', layer, function(evt) {
        var bounds = mapwidget.calculateBounds().transform( mapwidget.projection,mapwidget.displayProjection ).toBBOX();
        $.ajax( {
            url: map_div.attr( 'data-url' ),
            cache: false,
            data: {
                'bbox': bounds,
                'srid': mapwidget.projection.projCode.substring(5)
            success: function( data ) {
                if (data.success) {
                    if (data.features.length) {
                        replace_features( data.features );
                } else {
                    console.log( 'Failure during query: '+data.message );
            error: function( jqxhr, status, errThrown ) {
                console.log( 'Query failed: '+ status + ' '+errThrown );

We register a callback handler that will be called whenever the user stops panning and/or zooming on the map.  We get the bounding box, then pass it via ajax to the django back-end (which we'll see in a moment).  If the results come in, we pass them to our layer.

I have to admit that the transform seems backward here; OpenLayers documentation just doesn't seem to specify in what coordinate-space the various calls will return their results. I wound up with the "right" coordinates AFAICS, that is, GPS-style SRID 4326 lat/long values, but it really seems I should have wound up with "Googley" coordinates with that series of calls.

The django handler is pretty straightforward, in this case we're going to do a query to produce the first 100 results within the current bounding box:

@permission_required( 'yourapp.perm' )
def server_features( request ):
    """Retrieve the set of features to display on map via bounding box"""
    bbox = request.REQUEST.get( 'bbox' )
    srid = request.REQUEST.get( 'srid' )
    srid = 4326 # hard-coded, as the srid seems backwards
    if not bbox:
        return { 'error': True, 'message': _("Require a bounding box") }
        a,b,c,d = [float(x) for x in bbox.split(',')]
    except Exception as err:
        return { 'error': True, 'message': _("Require bounding box in a,b,c,d format") }
    bounds = 'SRID=%s;MULTIPOINT (%s %s,%s %s)'%(
    servers = models.Server.objects.filter(  
        gps__bboverlaps = bounds
        'error_count', 'warning_count','identity',
    return {
        'success': True,
        'features': prepare_features( [
            [server.gps, html_popup(server)]
            for server in servers

And the last little bit is the function prepare_features:

def prepare_features( info ):
    from olwidget import utils
    wkt_array = []
    for geom, attr in info:
        wkt = utils.get_ewkt(geom)
        if isinstance(attr, dict):
            wkt_array.append([wkt, utils.translate_options(attr)])
            wkt_array.append([wkt, attr])
    return wkt_array

Which is just ripped out of the InfoLayer's prepare method.  You set the name of your template on the InfoMap and you are off to the races with your custom dynamically updating map that lets you browse your database of locations/geometry without needing to load all of the geometry at once.  Your back-end queries can use whatever GeoDjango query mechanisms you need to build the query set, so you can sort by whatever parameters you like to get the most "important" data on the map.


Comments are closed.


Pingbacks are closed.