Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#249): Adds default geopoint question type #300

Draft
wants to merge 40 commits into
base: main
Choose a base branch
from

Conversation

latin-panda
Copy link
Collaborator

@latin-panda latin-panda commented Feb 6, 2025

WIP

  • To finish tests in vue
  • To add node options to BaseNode (depends on how many file changes otherwise, it will be submitted in a separate PR)
  • To finish manual testing and record evidence

Closes #249

I have verified this PR works in these browsers (latest versions):

  • Chrome
  • Firefox
  • Safari (macOS)
  • Safari (iOS)
  • Chrome for Android
  • Not applicable

What else has been done to verify that this works as intended?

The test plan is here

Test videos:
[To be uploaded soon]

Why is this the best possible solution? Were any other approaches considered?

  • A separate component in the Web Forms client now handles the readonly representation of Geopoint data. This component is reused in both Geopoint Note and Geopoint Input, ensuring consistent display of the same data.
  • The Geopoint Codec now provides more robust support for Geopoint data tuples with type checking, that clarifies the expected representation of this data.

How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?

This work should support the default behavior of Geopoint input and no regression are expected.

Do we need any specific form for testing your changes? If so, please attach one.

The test forms are at the end of this comment

What's changed

  • Adds a demo form for geopoint: 1-geopoint.xml
  • Adds a geopoint note in the note's demo form: 2-all-possible-notes.xml
  • Adds new scenario tests for geopoint input (bind-types.test.ts), there are already some scenario smoke tests working with geopoint.
  • Adds a geopoint input and geopoint readonly components in web-forms (vue client)
  • Registers geopoint in the input node and in note node
  • Adds a geopoint codec
  • Adds node options concept for question attributes - see input control

Copy link

changeset-bot bot commented Feb 6, 2025

⚠️ No Changeset found

Latest commit: 1d9514c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Comment on lines 22 to 24
this.rows = Number(element.getAttribute('rows')); // ToDo: Make parser function, validate NaN
this.accuracyThreshold = Number(element.getAttribute('accuracyThreshold')); // ToDo: Make parser function, validate NaN
this.unacceptableAccuracyThreshold = Number(element.getAttribute('unacceptableAccuracyThreshold')); // ToDo: Make parser function, validate NaN
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not suggesting a change this early on. Just adding a minor "yes and" on these TODO comments for additional context. There are two gaps here, and they apply in a few other places too:

  • accuracyThreshold and unacceptableAccuracyThreshold are specified as decimals. We'll want to make sure that whatever parsing we do accounts for that (including potential float rounding errors).
  • rows doesn't specify its type explicitly, but I think it's safe to assume it should be an integer. Same thing about parsing the type, different type to account for.

I wish we'd been tracking these cases sooner, because like I said there are others!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two gaps here, and they apply in a few other places too:

@eyelidlessness Do you mean in the project in general or in this new PR code?

@@ -11,10 +12,18 @@ export class InputControlDefinition extends ControlDefinition<'input'> {

readonly type = 'input';
readonly appearances: InputAppearanceDefinition;
readonly rows: number | null;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rows implementation will be completed in this issue: #301

@latin-panda latin-panda force-pushed the adds-default-geopoint-question-type branch from 414d3ee to 001fdf8 Compare February 10, 2025 22:02
Comment on lines 156 to 158
// ToDo: if one is missing, it still need to know the position of the others.
// ToDo: Change the value type to object? or default to negative / zero number?
props.node.setValue([latitude ?? 0, longitude ?? 0, altitude ?? 0, accuracy ?? 0].join(' '));
Copy link
Collaborator Author

@latin-panda latin-panda Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eyelidlessness I'd like to know your opinion.

First question:
The order of these values in the string is essential for locating them correctly, avoiding confusion between altitude and accuracy.
I noticed that altitude isn't always available (e.g., when testing with WiFi on a desktop). To preserve the position, what do you think about setting a zero or null default value?
The other option is to assume that if only three values come in the string, the third one is accuracy and that altitude wasn't provided 🙁 (which needs to be explained in the documentation for anyone making a client that connects with the engine)

Second question:
We need to create a new codec that builds on the one in XPath you just merged and call it in getSharedValueCodec, correct? It looks like ValueTypePlaceholderCodec doesn’t handle this automatically and is just a placeholder.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First question.....

All the JR imported tests in the scenario suite use a string with the four values (latitude, longitude, altitude, accuracy), and zero is used.

Maybe zero is the way to go 🤔; I haven't found anything explicit about this in the documentation of XForms

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be worth taking a step back. The intent of the engine/client interface—in this case InputNode—is to provide a representation of an <input> (in ODK XForms terms) which is most useful for clients, e.g. to provide a UI. The intent of ValueCodec is to decode readable values in that representation (i.e. InputNode.currentState.value), and to encode that representation of a value back to a "node value" (for submission, as serialized XML; for evaluation in XPath expressions).

It's generally up to us what that representation will be. So far, we've usually determined that by choosing a platform type with the closest semantics to the ODK XForms type: e.g. int <-> bigint, decimal <-> number.

There's an added wrinkle in this case, because there are currently two representations of the same type (geopoint) of value:

  • In @getodk/xpath, decoding a geopoint node value as an object which is pretty much a subset of the web standard GeolocationCoordinates
  • In @getodk/xforms-engine, encoding a geopoint node value from a GeoJSON Point (see also the semi-official type definitions)

If we're going to share the encode/decode logic for these existing cases, it seems pretty likely (unless we have a good reason otherwise) that we're going to want a representation of the value which is either:

  1. One of the two we already use
  2. A combined interface which is a superset of both
  3. Some other representation entirely???

Option 1 pretty much designs itself:

import { Point as GeoJSONPoint } from 'geojson';

interface GeopointRuntimeValue {
  readonly latitude: number;
  readonly longitude: number;
  readonly altitude: number; // Or `number | null`?
  readonly accuracy: number; // Or `number | null`?
}

type GeopointInputValue =
  | GepointRuntimeValue
  | GeoJSONPoint;

class GeopointValueCodec extends ValueCodec<GepointRuntimeValue, GeopointInputValue> {
  /**
   * @todo Implementation will:
   *
   * 1. Decode `GeopointRuntimeValue`, pretty much reusing the logic in `@getodk/xpath`
   * 2. Encode `GeopointInputValue` by detecting whether input is a `GeoJSON` point or a `GeopointRuntimeValue`, and then:
   *     - if the former, reuse logic for GeoJSON external secondary instances
   *     - if the latter, invert the decode logic
   */
}

Coming back to the question of what to do when altitude isn't available, we can make it nullable (like it is in the web standard GeolocationCoordinates), or default to 0 ... in the runtime representation (GeopointRuntimeValue). There's trade-offs either way: nullable is closer to the platform interface, but we can't distinguish a node value's 0 as missing or sea level. In either case, we have to encode a missing value as 0 (as far as I know, the altitude substring isn't optional).

Option 2 (combined superset interface) would make decode a little more complex (assigning the same sub-values each in two places), and simplify the encode logic (only need to reuse logic from GeoJSON external secondary instances).

Option 3 (a completely new representation)... I'd be open to it, but I would want to justify it by referencing something that's successful in projects with strong geo background. But my gut tells me that most representations of a geopoint are going to look very similar to one of the representations we already use.


In any case, I'd like to see if we can settle this aspect of the design before going too much further with the client integration. Whatever the representation we end up with, I don't think that we want the client to be responsible for decoding or encoding node values, since that's an engine responsibility everywhere else.

Copy link
Collaborator Author

@latin-panda latin-panda Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the chat earlier! This is a quick summary of the plan:

First round:
Build a normal geopoint's engine codec, in which runtime value representation is the shape of GeolocationCoordinates

Second round:
Evaluate the codec and see how to fit GeoJSON; maybe external secondary serialization to use the same encoding logic.

Third round:
Evaluate sharing between xpath and xform-engine packages.

After evaluating the second or third, we might decide that these are out of scope and create GH issues for those.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Work in progress

@@ -0,0 +1,25 @@
export const parseToInteger = (value: string | null): number | null => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another possible place for this is in packages/xforms-engine/src/lib

@@ -61,6 +98,7 @@ export class InputControl<V extends ValueType = ValueType>
// InputNode
readonly nodeType = 'input';
readonly appearances: InputNodeAppearances;
readonly nodeOptions: InputNodeOptions<V>;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't decided yet if I'll add this to BaseNode in this PR on the following one. If it's >20 file changes, I'll open a separate PR

@latin-panda
Copy link
Collaborator Author

latin-panda commented Feb 19, 2025

@eyelidlessness, if you have time, could you please review this draft?

I still have the following pending items that I'll be working on in the meantime:

  • [In progress] To finish vue tests
  • [Done] To finish scenario tests (there are a couple I want to add)
  • To add node options to BaseNode (depends on how many file changes; otherwise, it will be submitted in a separate PR)
  • To finish manual testing and record evidence

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Form filling: Geopoint with no appearance
2 participants