ODK geoshape/geotrace/geopoint to KML

(or "More Fun with Repeat Groups!" :slight_smile: )


Converting ODK's geoshape, geotrace and/or geopoint results into a more widely supported GIS format, eg KML, gets asked a lot on the Forum [a quick search for "KML" returns 125 hits!]. Unfortunately, there are no native XForm tools to perform such conversions in an ODK form, so typically you have to export submissions and perform some post-processing to convert these geo-spatial properties before importing your data into a GIS tool for further analysis or display.

I was inspired by a recent Forum posting from @eddy_rellum; it seemed like it should be technically feasible to generate KML from within an XForm, so I decided to have a crack at it. This is the result.


To begin, here is a map of the Feilding, New Zealand, with a geoshape (via Enketo) outlining the downtown area.


The first problem is extracting the latitude, longitude, etc of every point of the geoshape, which consists of 6 geopoints each separated by a ';' character. [If you read the geofence solution I recently posted to the Forum then a lot of this will be familiar, but I'll repeat the relevant steps here for completeness]. Each geopoint consists of an latitude, longitude, altitude and accuracy. The altitude and accuracy may be 0 (or 0.0), especically when selected from a map, but in general these can be real numbers and our solution must handle such. The above map produces:

Geopoints:           #1            |           #2            |           #3            |           #4            |           #5            |           #6
Geoshape: -40.212727 175.558409 0 0;-40.228628 175.555857 0 0;-40.232441 175.573712 0 0;-40.220068 175.577476 0 0;-40.217184 175.583143 0 0;-40.212727 175.558409 0 0
List:         1          2      3|      4          5      6|      7          8      9|      10         11    12|      13         14    15|      16         17    18 19
            Lat1       Long1  Al1|    Lat2       Long2  Al2|    Lat3       Long3  Al3|     Lat4      Long4  Al4|     Lat5      Long5  Al5|     Lat6      Long6  Al6
Index:    0                       1                         2                         3                         4                         5               

If you instead view the geoshape as a space-separated list then you can identify the index of each desired latitude, longitude and altitude needed for KML (see above). Note, we are not interested in the accuracy value for this conversion.

Step 1. First, using count-selected() we can calculate how many geopoints are in the geoshape. Each geopoint actually consists of only three list elements, because the accuracy value of the previous geopoint is prepended to the latitude of the latter, eg "0;-40.228628", except for the last geopoint. Hence

numpoints = (count-selected(${geoshape})-1) div 3)

Step 2. Next, we iterate thru each geopoint in the geopshape by implementing a ODK repeat group, with a count of ${numpoints}

begin_repeat | geopoints | repeat_count = ${numpoints}

Step 3. Then, within our repeat group, we convert each geopoint to its equivalent KML coordinate format.

3a. In order to simplify the calculations, we calculate the 0-indexed position in the list for the repeat iteration corresponding to the current point in the geoshape:

index = position(..) - 1

3b. Now we extract the latitude, longitude and altitude of the current geopoint. The longitude and altitude are easy, because their values are always on their own in the list. Specifically, they are:

long = selected-at(${geoshape}, ${index}*3 + 1)
alt = selected-at(${geoshape}, ${index}*3 + 2)

3c. Getting the latitude is a bit trickier, because in all cases except the first, the latitude value in the list is prepended with the accuracy of the previous geopoint, which might be "0;", "0.0;" or an arbitrary real number "0.12345;". We can use the new ODK substring-after() XPath function strip off this unwanted prefix:

latstr = selected-at(${geoshape}, ${index}*3 + 0)
lat = if(${index}=0, ${latstr}, substring-after(${latstr}, ';'))

Step 4. Now that we have extracted all the required geopoint components, we can reformat them into a single equivalent comma-separated KML coordinate using

coord = concat(${long}, ',', ${lat}, ',', ${alt})

Step 5. Our repeat group performs this conversion for all the geopoints in the geoshape. At the end, we just need to collect these coords up and put them in a (space-separate) KML <coordinates> list. This can be accomplished using the relatively obscure and little used ODK join() function:

coordinates = concat('<coordinates>', join(' ',${coord}), '</coordinates>')

We now have a KML representation of our original geoshape. The best way to view this is as a KML Polygon, where these coordinates are used to define the outer boundary LinearRing of the polygon. The necessary XML markup for a KML Polygon looks like:

kml = concat('<Placemark><name>geoshape2kml</name><Polygon><outerBoundaryIs><LinearRing>',${coordinates},'</LinearRing></outerBoundaryIs></Polygon></Placemark>')


Below I've attached an XLSForm which implements this geoshape2kml process. The user first draws a geoshape on a map, and the form will then convert this to an equivalent KML Polygon placemark. Load the form into https://opendatakit.org/xlsform/, plot out a geoshape, and then load the resulting KML into a suitable KML viewer (eg I used http://display-kml.appspot.com/ above) to see the result.

geoshape2kml.xls (21.5 KB)


In actuality, a geotrace is syntactically identical to a geoshape, so the process for converting a geotrace to KML is pretty much the same as for a geoshape. The only real difference is how the list of coordinates are interpreted. The equivalent of a geotrace in KML is a Path, which is represented using a LineString. After converting our geotrace's points to a list of KML coordinates (see geoshape2kml above), the necessary XML markup for a KML Path looks like:

kml = concat('<Placemark><name>geotrace2kml</name><LineString>',${coordinates},'</LineString></Placemark>')

geotrace2kml.xls (21.5 KB)


Although it doesn't require repeat groups, for completeness I thought I'd include how to convert a single geopoint to an equivalent KML placemark. Again, a geopoint consists of a space-separated latitude, longitude, altitude and accuracy. eg

geopoint = "-40.226269 175.565817 0 0"

All that is necessary to convert this is to swap the latitude and longitude (KML wants longitude first), drop the accuracy (which KML doesn't use), and use a comma separator instead of a space. The above GPS location in KML looks like:


Because geopoints use a space separator, all we do is use the ODK selected-at() function to pull out the required values, and the ODK concat() function to rearrange them into a KML-compatible string:

long = selected-at(${geopoint},1)
lat = selected-at(${geopoint},0)
alt = selected-at(${geopoint},2)

and finally

coord = concat(${long}, ',', ${lat}, ',', ${alt})

If you wish to generate an actual KML Placemark for this, the necessary XML markup looks like

kml = concat('<Placemark><name>geopoint2kml</name><Point><coordinates>',${coord},'</coordinates></Point></Placemark>')

geopoint2kml.xls (20 KB)


Hopefully these may be of some use if, for example, you want to add a directly-importable KML Placemark in your form (and for whatever reason you cannot use @ggalmazor's wonderful new Briefcase GeoJSON export to extract geo-referenced submission data into a more consumable GIS format).

Much as with my earlier geofence_v1 solution, this isnt a terribly efficient way to convert things to KML - it would be far easier to accomplish via a dedicated geoshape-to-kml() XPath function, and it requires imposing a distracting repeat group in your form that users must navigate thru. But if you are desparate to submit KML then the solutions provided here should work.


I love these posts!

You can also go full circle by using Enketo's built-in KML viewer in an empty geoshape widget to test the result (click light gray "KML" - sorry not sure why that is light gray...).

1 Like

I tried to use the geotrace to kml tool, but when sending the form, I get this error message. What could be the problem?

These new functions were introduced in ODK Collect v1.19

This is for odk collect, right? This works for kobotoolbox? Or kobocollect?

Correct. You will have to wait for Kobo to update KoboCollect to a more recent version Collect with the necessary new substring-after() function. However, you can run these forms under KoboToolbox today using Enketo instead, which already has substring-after()

I understand. I Think that may take some time, because in the Google Play its last update was in April 2018. Anyway thank you very much for your prompt response

If you are desperate, you should be able to run latest version of ODK Collect against KoboToolbox. If you hop onto the KoboToolbox Forum you can probably find instructions; eg https://community.kobotoolbox.org/t/help-how-to-link-kobo-server-to-odk-collect-on-android-tablet/2483

Hi Xiphware,

I haven't been oin the forum for some weeks and now see this solution. Many thanks for diving into this! I will certianly try it! Great job!

1 Like