Using GeoServer, Openlayers, and CQL Filters

I received a request this week to put Census Questionnaire Centers (QAC) on a web app ASAP. In case you don’t follow the Census’ every move:

Questionnaire Assistance Centers (QACs) are spaces, donated by community partners, where staff from the Local Census Office or the partner organization are available to answer questions about completing the questionnaire, provide special language assistance and answer general questions.

This is a short term thing - the QACs are only open for 4 weeks or so - and the only initial requirement was dumping the points on the map and showing information when you click on them. So I dumped the shape file into Postgres, published the layer with GeoServer, and added it to an app as a vector layer (KML).

For the most part this code was proudly pilfered from one of the OpenLayers samples. Here we’ll add the layer to the map and set some select events. I made the census_qac_layers a global variable so I could get at it later. Note I took out the enormous URL so the page wouldn’t explode, but this was it (it will probably ask you to pull it up in Google Earth).

[sourcecode language=’javascript’]

// QAC Centers for Census
census_qac_centers = new OpenLayers.Layer.Vector(“Census QAC Centers”, {
projection: map.displayProjection,
strategies: [new OpenLayers.Strategy.Fixed()],
protocol: new OpenLayers.Protocol.HTTP({
url: “http://enormous-url“,
format: new OpenLayers.Format.KML({
extractStyles: true,
extractAttributes: true
})
})
});
census_qac_centers.setVisibility(false);
map.addLayer(census_qac_centers);
select = new OpenLayers.Control.SelectFeature(census_qac_centers);
census_qac_centers.events.on({
“featureselected”: onFeatureSelect,
“featureunselected”: onFeatureUnselect,
“stopClick”: true
});
map.addControl(select);
select.activate();

1

And here are the onFeatureSelect and onFeatureUnselect functions we referenced. They'll handle creating the popups.

[sourcecode language="javascript"]
/* Functions for Census QAC Centers */
    function onPopupClose(evt) {
            select.unselectAll();
        }
        function onFeatureSelect(event) {
            var feature = event.feature;
            // Since KML is user-generated, do naive protection against
            // Javascript.
            var content = "<h2>"+feature.attributes.name + "</h2>" + feature.attributes.description;
            if (content.search("<script") != -1) {
                content = "Content contained Javascript! Escaped content below.<br />" + content.replace(/</g, "<");
            }
            popup = new OpenLayers.Popup.FramedCloud("chicken",
                 feature.geometry.getBounds().getCenterLonLat(),
                 new OpenLayers.Size(100,100),
                 content,
                 null, true, onPopupClose);
            feature.popup = popup;
            map.addPopup(popup);
        }
        function onFeatureUnselect(event) {
            var feature = event.feature;
            if(feature.popup) {
                map.removePopup(feature.popup);
                feature.popup.destroy();
                delete feature.popup;
            }
        }

Done and done. Our map now has the pox, and each pustule will pop up the QAC information. I gave the KML output a little styling, which is literally as simple as dropping a couple of files like this in your layer folder (here’s description.ftl):

[sourcecode language=”text”]

${partner_na.value}

${street.value}

${phone.value}

Hours: ${hours.value}



Langauges served: ${language_s.value}

1

Being at least nominally an <a href="http://agilemanifesto.org/">agile</a> guy, I (a) release early and often and (b) fully expect changes. So we tossed it out there, looked at it, and decided it didn't make sense to show all the QAC's at once. What people would really want to do is show just the locations that support their language. I don't want to click through 111 locations hunting for a QAC that supports Hindi.

The last thing I wanted to do was make a bunch of separate layers/views/styles for different languages, and an individual QAC can support multiple languages, further complicating the matter.

<a href="http://www.loc.gov/standards/sru/specs/cql.html">CQL</a> to the rescue.

<blockquote>CQL, the Contextual Query Language, is a formal language for representing queries to information retrieval systems such as web indexes, bibliographic catalogs and museum collection information. The design objective is that queries be human readable and writable, and that the language be intuitive while maintaining the expressiveness of more complex languages.</blockquote>

CQL was developed by the Library of Congress and OGC used it as a basis for its filter encoding spec (I *think*). GeoServer fully supports the OGC filter encoding sped, but sending it over HTML makes for a horrid mess, as you writing and URL encoding XML. GeoServer also supports plain old CQL, which is quite a bit easier. I needed to add a line to the layer declaration to hold a CQL_Filter parameter.

[sourcecode language="javascript"]

// QAC Centers for Census
    census_qac_centers = new OpenLayers.Layer.Vector("Census QAC Centers", {
        projection: map.displayProjection,
        strategies: [new OpenLayers.Strategy.Fixed()],
        protocol: new OpenLayers.Protocol.HTTP({
            url: "http://enormous-url",
            'params' : { 'CQL_FILTER' : ''},
            format: new OpenLayers.Format.KML({
                extractStyles: true,
                extractAttributes: true
            })
        })
    });

Next I made a standard HTML radio button list, with the value being the language.

[sourcecode language=”html”]

English

Arabic

Chinese

French

1

Etc. Now for some jQuery goodness.

[sourcecode language="javascript"]

    // Census QAC Stuff
    $("input[name='qac_centers']").attr("checked", false)
    $("input[name='qac_centers']").change(
        function()
        {
            census_qac_centers.setVisibility(true);
            filterval = "language_s like '%" + $("input[name='qac_centers']:checked").val() + "%'";
            census_qac_centers.refresh({
                force: true,
                params: {
                    "CQL_FILTER": filterval
                }
            });
        }
    );

Let’s walk through this. First, we set all the radio buttons to be unchecked (Firefox likes to remember that stuff from session to session). Next we detect whenever a change occurs (i.e. a radio button is checked) and run a function. We set the layer to visible, grab the language value of the checked radio button, and create the CQL filter. It’ll end up looking something like this, where language_s is the field name we’re querying:

[sourcecode language=”sql”]
language_s like ‘%Spanish%’

1

I'm doing a like operation because the field has a comma delimited list of languages it supports. This next bit does the heavy lifting.

[sourcecode language="sql"]
census_qac_centers.refresh({
      force: true,
      params: {
                    "CQL_FILTER": filterval
                }
 });

Here we take the CQL_FILTER parameter we added to the layer, and we’re dropping in the CQL we created. The layer is refreshed with the new URL, and only the features that match our CQL filter will come back. A user clicks on German, only the German QAC’s show up.

The CQL Filter option is available on any layer in GeoServer (not just vector), and with the parameter option in OpenLayers it’s very easy to dynamically change the content of a layer. We were able to turn around this request in an extremely short amount of time (a couple of hours, a large portion of which was navel-gazing design) and in a way that’s easy to extract from the application when the QAC’s go away in a month.

You can view it on GeoPortal, second accordion tab on the left.