(or "More Fun with Repeat Groups!" )
Background
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.
geoshape2kml
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>')
Done!
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)
geotrace2kml
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)
geopoint2kml
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:
175.565817,-40.226269,0
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)
Discussion
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.