or, "programmatically set a geopoint when the user changes the value of a control/question"
What high-level problem are you trying to solve?
This is a proposal to expose the odk:setgeopoint action functionality that currently exists in both Collect and Enketo, which can be triggered in response to an xforms-value-changed event; that is, programmatically set a geopoint when the user changes the value of a control/question. This functionality already works today - in XForms - but is not yet exposed in XLSForm, so currently the only way to add this functionality into your form is by manually editing the XForm XML file.
[please skip to the summary at end if you are less interested in understanding the thought process that got there... ]
Background
Currently there is existing functionality in both Collect and Enketo to programmatically capture the current GPS location, instead of requiring user input via an explicit geopoint question. This is accomplished via the odk:setgeopoint action. This action, along with the setvalue action, are the two ODK supported XForm actions. Both of these actions can be invoked in response to events; specifically, the odk-instance-first-load, xforms-value-changed, and odk-new-repeat events.
The odk:setgeopoint action permits a form designer to programmatically capture the current GPS location when the form is first started (odk-instance-first-load event), or whenever the user answers or changes a specific question (xforms-value-changed event), or when the user adds a new repeat group (odk-new-repeat event). There are examples of all three cases in the ODK XForms spec. The specific XForm case we want exposed via XLSForm is this:
<bind nodeset="/data/my_text" type="string" />
<bind nodeset="/data/my_text_changed" type="string" />
<bind nodeset="/data/my_current_location" type="string" />
...
<input ref="/data/my_text">
<setvalue event="xforms-value-changed" ref="/data/my_text_changed">Value changed!</setvalue>
<odk:setgeopoint event="xforms-value-changed" ref="/data/my_current_location" /> <======= MISSING IN XLSFORM!
</input>
However, presently only the odk-instance-first-load invoked GPS capture is exposed in XLSForm, as the XLSForm start-geopoint question. [aside: start-geopoint is predominantly used to 'warm-up' the GPS sensor for subsequent geo-questions in the form, but what it is actually doing is capturing the current GPS location - via the odk:setgeopoint action - upon initializing a new form, thereby warming up the GPS for later use].
Related to this discussion is the xforms-value-changed event which, in effect, is presently exposed in XLSForm as the trigger column. Basically, what happens in XLSForm is that when a question row (the 'dependent') has a trigger associated with it, the trigger field identifies the triggering control question (the 'antecedent'), which when its value changes will cause, or 'trigger', the dependent question's calculation to be executed and thus assign it a new value. This triggering behavior is now an established pattern that makes sense to XLSForm builders, although what is really happening under-the-covers in the XForm itself is that the triggered calculation is actually defined against with the antecedent control (!), telling it to perform said calculation whenever that control's value changes, and then update the specified target instance element with the result. So although the trigger (aka antecedent) and calculation appears to be associated with the desired target row (aka dependent) in XLSForm, in the actual XForm definition the location in the form definition where this is actually defined is somewhat reversed. To give a more concrete example, the following XLSForm
translates to this XForm:
<?xml version="1.0"?>
<h:html
xmlns="http://www.w3.org/2002/xforms"
xmlns:h="http://www.w3.org/1999/xhtml"
xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:jr="http://openrosa.org/javarosa"
xmlns:orx="http://openrosa.org/xforms"
xmlns:odk="http://www.opendatakit.org/xforms">
<h:head>
<h:title>Simple Trigger</h:title>
<model odk:xforms-version="1.0.0">
<instance>
<data id="SimpleTrigger" version="1">
<temp/>
<temp_ts/>
<meta>
<instanceID/>
</meta>
</data>
</instance>
<bind nodeset="/data/temp" type="int"/>
<bind nodeset="/data/temp_ts" type="dateTime"/>
<bind nodeset="/data/meta/instanceID" type="string" readonly="true()" jr:preload="uid"/>
</model>
</h:head>
<h:body>
<input ref="/data/temp">
<label>Enter the current temperature</label>
<setvalue ref="/data/temp_ts" event="xforms-value-changed" value="now()"/>
</input>
</h:body>
</h:html>
As you can see, all the actual triggering stuff - the xforms-value-changed event, the setvalue action, and the calculation definition - actually reside under the antecedent triggering question's XForm definition. Whereas the dependent question - ie the entire temp_ts row - is merely the target of the setvalue action's ref.
More background: Current XLSForm trigger behavior
As described above, IMO the existing trigger column in XLSForm basically represents the xforms-value-changed event; that is, when the specified triggering question's value changes (the antecedent), perform some action against the control for that row (ie the target, or dependent). Presently, this action is always a setvalue action, which either (a) evaluates the associated calculation and store the result in the specified target's element, or (b) if the calculation is empty then clear the target element's value. From https://xlsform.org/en/#trigger:
...An important and powerful difference with regular calculations is that the calculation value with a trigger may also be empty, which serves to clear a value from the form.
This last point is quite important, because it stipulates that irrespective of whether the triggered calculation contains something or is empty, you will always perform a setvalue action against the target element whenever the triggering element's value changes!
Whereas, what is desired here - and what is already possible to define in the raw XForm... - is to instead perform a odk:setgeopoint action when the triggering element's value changes. That is, if we consider the XLSForm trigger column does effectively just represent the xforms-value-changed event, then it rather needs to be 'decoupled' somewhat from the otherwise always-implied setvalue action; there are other valid actions you can associate with xforms-value-changed event.
Requirement
To reiterate, the desired xforms-value-changed event-triggered odk:setgeopoint action already works; what is needed is just to appropriately expose this in XLSForm; specifically, in a manner that remains consistent with and sympathetic to the existing behavior of XLSForm triggers (which are all currently setvalue-only).
A trigger-based XLSForm which updates a geopoint, in response to answering or changing the value of a form question, I think would most intuitively appear in an XLSForm something along the lines of:
This is consistent with existing trigger functionality, which can be applied to arbitrary data types; eg dateTime, text, calculations, etc. From XLSForm trigger spec:
So you might expect the same could be applied to trigger programmatically setting a geopoint. But now instead of, say, setting a dateTime to now()
when the trigger ${temp}
is fired, we instead want to set a geopoint value using odk:setgeopoint
.
This immediately suggests a possibility of perhaps just introducing a new function, say setgeopoint()
, with which to put in the calculation field to indicate setting the geopoint value with odk:setgeopoint instead. However, in all other contexts, the XLSForm calculation field is just that - it specifies the actual XPath expression to perform, and there is no actual valid 'setgeopoint()' function [indeed, it was deliberate decision not to make setgeopoint a function; see Spec addition proposal: location preload - #9 by martijnr].
Further, adding something into the existing calculation field for this could lead to form designers thinking they can use it in other calculations in other contexts, when it fact it is not and could only be used in conjunction with the specific xforms-value-changed event trigger. It is for these reasons that I'm not keen on adding what would basically be 'semantic sugar' to the existing XLSForm calculation field to accomplish this; it doesn't feel right.
Proposal
As stated earlier, I think it is natural to re-use the existing XLSForm trigger mechanism to expose an xforms-value-changed event triggered odk:setgeopoint action, because this is consistent with the existing usage of (event) triggers in XLSForm. Therefore, what is needed is a new means in XLSForm to override the current always-implied setvalue action. Unfortunately, as described above, it is not sufficient to use the lack of a calculation to perhaps identify when not to perform a setvalue because an empty calculation is already defined to mean simply blank the dependent value!
It is for this reason that I think it may be unavoidable to have to introduce a new column type to the XLSForm survey worksheet [as loath as I may be to add new columns...]; we legitimately want to re-use the trigger column, but we cant repurpose the calculation column, and none of the other existing survey worksheet columns seem to lend themselves to this purpose (eg the existing optional parameters column cannot be used, because this applies the parameters into the dependent question, whereas the trigger stuff actually resides under the antecedent question, as described earlier)
An additional XLS column in the survey worksheet to indicate performing a odk:setgeopoint for the (presumably specified) trigger could be a simple as a dedicated column with the value true. eg
Here, if a row has a trigger defined, and setgeopoint = true, then perform a odk:setgeopoint action storing a GPS location for that row. Otherwise (if setgeopoint is missing, or false) then perform the de facto a setvalue action and store whatever the calculation field says (or blank if empty). That is, the setgeopoint = true overrides the trigger's default setvalue behavior.
The main disadvantage I see with this solution is that it may not scale well should we want to introduce further new actions in the future (eg perhaps automatically take a picture?, or make an API call when a question is answered? etc). With this approach every new action type would require adding a new column. Further, some of these new actions might involve additional parameters (eg a URL for making an API call...), which may mean adding additional columns to specify action-specific parameters.
So instead I might propose adding a new fairly generic action column - called say "action" - which, when non-null, specifies what specific XForm action to take in response to the trigger, the default being a setvalue action (with its implied calculation...) should no action be specified, in order to maintain backward compatibility with how things work today. That is
(I dont see why not use the actual odk:setgeopoint XForm action name here)
I also see no need to complicate things further by permitting more than one action [each with potentially its own parameters...], although in principle this can certainly be supported in XForms. But an inability to express in XLSForm having multiple antecedent questions performing actions against the same target question is no worse than what already exists today. Although in XForms you may have multiple antecedent questions performing multiple setvalue (each with their own calculation...) against the same ref element [remember, the triggering stuff is actually defined against the triggering question, not the target!], there isnt any way to express this in XLSForm, because each row can only define at most one trigger. Which is to say, I think restricting it to a single action column where you can optionally specify a single action may suffice.
Other options
Another option, which is ostensibly equivalent to the above, is instead of having a separate action column containing the desire action override, instead put the action specifier alongside the triggering question identifier in the existing trigger column. eg
Given that the odk:setgeopint doesn't require any further parameters itself, this would be functionally identical to the proposed new explicit action column approach. My personal preference is however for an explicit 'action' column, because I think it could be useful going forward to be able to distinctly identify actions in an explicit manner in XLSForm, outside of the ostensibly xforms-value-changed only event-specific 'trigger' column. Indeed, XForm actions and XForm events are quite distinct from each other, so the ability to somewhat maintain a (extensible) clear separation may become more beneficial as ODK event-actions evolve and mature.
Summary
In summary, this is a proposal to expose existing the odk:setgeopoint action functionality which exists in both Collect and Enketo, in XLSForm when the user answers or changes a form control value; that is, in response to an xforms-value-changed event. This will permit programmatically capturing a geopoint when the user answers a question. This functionality already works but only in XForms; it is not yet exposed in XLSForm, so currently the only way to add this functionality into your form is by manually editing the XForm XML file. The proposal is to add a new 'action' column to XLSForm with which to specify performing a odk:setgeopoint action in response to the trigger instead of the default setvalue calculation.
Comments and suggestions of alternative or better solutions most welcome. We have an immediate need for this existing ODK functionality to be exposed in XLSForm, and given that the underlying functionality already works, in XForm, and doesnt actually require any changes to either Collect or Enketo to support, it would be nice to get something suitable defined for XLSForm soon and then proceed in making the necessary code changes in pyxform to consume it. Many thanks,
- Gareth