Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
5b292dc
Adds demo xml form for geopoint
latin-panda Feb 4, 2025
321d486
Adds watch for geolocation
latin-panda Feb 5, 2025
f919181
Adds buttons logic
latin-panda Feb 5, 2025
c94d612
Styles UI and DOM structure
latin-panda Feb 6, 2025
1e08674
Node Definition for Geo types
latin-panda Feb 6, 2025
a10f171
Revert "Node Definition for Geo types"
latin-panda Feb 6, 2025
889fe90
Fixes type check in webapp
latin-panda Feb 6, 2025
da8d36c
Fixes lint and type check in webapp
latin-panda Feb 7, 2025
44655b6
Adds nodeOptions to access attributes
latin-panda Feb 7, 2025
b330e74
Adds parser functions for nodeOptions
latin-panda Feb 7, 2025
03fba0d
Extract node option parsers to a file and test coverage
latin-panda Feb 8, 2025
f1446c1
Input node options is optional
latin-panda Feb 8, 2025
362612f
Implement accuracy threshold logic in client vue component
latin-panda Feb 8, 2025
cd351bb
Fixes GitHub lint
latin-panda Feb 8, 2025
1ea7869
Implements geolocation error case and improves quality assessment logic
latin-panda Feb 10, 2025
dca688a
Implements readonly
latin-panda Feb 10, 2025
1b57c73
Implements saving when control leaves the viewport as the user scrolls
latin-panda Feb 10, 2025
36ea1d1
Fixes vue component type check warning
latin-panda Feb 10, 2025
05e12f4
Implements autosave when form submits for geopoint
latin-panda Feb 10, 2025
001fdf8
Improving code style
latin-panda Feb 10, 2025
cf21460
Adding scenario tests for input type geopoint
latin-panda Feb 11, 2025
059d4d4
Fixing title and icons
latin-panda Feb 11, 2025
79a5432
codec
latin-panda Feb 12, 2025
c2dd023
Fixes codec to work with Note and Integrate codec to Geopoint Vue Com…
latin-panda Feb 14, 2025
8fa68af
Refactored UI to match redesigned UI for geopoint
latin-panda Feb 15, 2025
b6e965c
Refactored Geopoint codec to support string in encode
latin-panda Feb 15, 2025
d54bb0a
Simplified decode and properly registered geopoint note
latin-panda Feb 15, 2025
c3d09fc
format
latin-panda Feb 15, 2025
36c483d
Format constant for accuracy quality
latin-panda Feb 17, 2025
e97cb69
Fixes scenario's geopoint tests
latin-panda Feb 18, 2025
443eb5c
Fixes type check in web forms package
latin-panda Feb 18, 2025
f907cc8
Fixes scenario smoke test
latin-panda Feb 18, 2025
9bdff51
Tiny improvements in code
latin-panda Feb 18, 2025
eefb1e9
UIUX Feedback: fix margins and text indentation. Try again is hidden …
latin-panda Feb 19, 2025
c9ed78b
Codec feedback - minimum valuable solution
latin-panda Feb 19, 2025
a50983a
Codec feedback - extended solution with tuple type check
latin-panda Feb 19, 2025
aa8c1ab
Codec feedback - extended solution codec fix
latin-panda Feb 19, 2025
cecd173
Codec feedback - extended solution - geopoint class generates runtime…
latin-panda Feb 19, 2025
f18e875
Adding JSDoc and removing unnecessary symbol
latin-panda Feb 19, 2025
1d9514c
Adding more scenario tests for geopoint codec
latin-panda Feb 19, 2025
c652c70
Feedback:
latin-panda Feb 21, 2025
21f8964
Adds scenario tests for input as GeopointValue object
latin-panda Feb 21, 2025
79b501f
Putting back the XML prolog
latin-panda Feb 21, 2025
d4e2066
Fixed bind type tests
latin-panda Feb 21, 2025
790c325
Implements state machine in Web Form client
latin-panda Feb 22, 2025
2714b21
Simplifying Geopoint tuple code and renaming NodeOptionsParser to Num…
latin-panda Feb 23, 2025
a0699d1
Moving code from Geopoint codec to Geopoint class
latin-panda Feb 23, 2025
241db93
Validates null island case
latin-panda Feb 23, 2025
1eca15a
Adds nodeOptions to base nodes
latin-panda Feb 24, 2025
89255da
Adding changeset
latin-panda Feb 25, 2025
37efc2f
Adding readonly nodeOptions null to node definitions
latin-panda Feb 25, 2025
80e5251
Refactores geopoint state machine
latin-panda Feb 27, 2025
62b4c6c
From Feedback: Truncating to max 3 decimals, space before meter unit
latin-panda Feb 27, 2025
bf60966
Resolves feedback: Refactors truncate function, rename number parser …
latin-panda Mar 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/violet-chefs-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@getodk/xforms-engine': minor
'@getodk/web-forms': minor
'@getodk/scenario': minor
'@getodk/common': patch
---

- Support for geopoint questions with no appearance
- Support for geopoint notes
86 changes: 86 additions & 0 deletions packages/common/src/fixtures/geopoint/geopoint.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?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>Geopoint</h:title>
<model odk:xforms-version="1.0.0">
<itext>
<translation lang="English (en)">
<text id="/data/intro:label">
<value>The browser will display a permission prompt to allow or block location access. Click 'Allow' to enable location services. If dismissed, the prompt may not appear again unless permissions are reset in browser settings.</value>
</text>
<text id="/data/location_no_autosave:label">
<value>Where are you filling out the survey?</value>
</text>
<text id="/data/location_with_big_threshold:label">
<value>Where are you within the target area?</value>
</text>
<text id="/data/location_with_default_attributes:label">
<value>Where are you within the default threshold area?</value>
</text>
<text id="/data/location_with_unacceptable_accuracy_only:label">
<value>Try to provide a more specific location</value>
</text>
</translation>
<translation lang="French (fr)">
<text id="/data/intro:label">
<value>Le navigateur affichera une demande d'autorisation pour permettre ou bloquer l'accès à la localisation. Cliquez sur 'Autoriser' pour activer les services de localisation. Si vous ignorez la demande, elle pourrait ne plus réapparaître, sauf si vous réinitialisez les autorisations dans les paramètres du navigateur.</value>
</text>
<text id="/data/location_no_autosave:label">
<value>Où remplissez-vous le sondage?</value>
</text>
<text id="/data/location_with_big_threshold:label">
<value>Où êtes-vous dans la zone cible?</value>
</text>
<text id="/data/location_with_default_attributes:label">
<value>Où êtes-vous dans la zone de seuil par défaut?</value>
</text>
<text id="/data/location_with_unacceptable_accuracy_only:label">
<value>Essayez de fournir un emplacement plus précis</value>
</text>
</translation>
</itext>
<instance>
<data id="1_geopoint" version="2025020401">
<intro/>
<location_no_autosave/>
<location_with_big_threshold/>
<location_with_default_attributes/>
<location_with_unacceptable_accuracy_only/>
<meta>
<instanceID/>
</meta>
</data>
</instance>
<bind nodeset="/data/intro" readonly="true()" type="string"/>
<bind nodeset="/data/location_no_autosave" type="geopoint"/>
<bind nodeset="/data/location_with_big_threshold" type="geopoint"/>
<bind nodeset="/data/location_with_default_attributes" type="geopoint" required="true()"/>
<bind nodeset="/data/location_with_unacceptable_accuracy_only" type="geopoint"/>
<bind nodeset="/data/meta/instanceID" type="string" readonly="true()" jr:preload="uid"/>
</model>
</h:head>
<h:body>
<input ref="/data/intro">
<label ref="jr:itext('/data/intro:label')"/>
</input>
<input accuracyThreshold="0" ref="/data/location_no_autosave">
<label ref="jr:itext('/data/location_no_autosave:label')"/>
<hint>(No autosave)</hint>
</input>
<input accuracyThreshold="100" unacceptableAccuracyThreshold="400" ref="/data/location_with_big_threshold">
<label ref="jr:itext('/data/location_with_big_threshold:label')"/>
<hint>(Acceptable accuracy: 100m, unacceptable: 500m)</hint>
</input>
<input ref="/data/location_with_default_attributes">
<label ref="jr:itext('/data/location_with_default_attributes:label')"/>
<hint>(Default acceptable accuracy: 5m, unacceptable: 100m)</hint>
</input>
<input unacceptableAccuracyThreshold="7" ref="/data/location_with_unacceptable_accuracy_only">
<label ref="jr:itext('/data/location_with_unacceptable_accuracy_only:label')"/>
<hint>(Unacceptable accuracy: 7m)</hint>
</input>
</h:body>
</h:html>
7 changes: 6 additions & 1 deletion packages/common/src/fixtures/notes/2-all-possible-notes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<read_only_int />
<read_only_int_value>3</read_only_int_value>
<note_calc_decimal_from_int />
<geopoint-note>38.253094215699576 21.756382658677467 0 150</geopoint-note>
</group>
<meta>
<instanceID />
Expand All @@ -38,6 +39,7 @@
<bind nodeset="/data/group/note_calc_decimal_from_int" type="decimal"
calculate="/data/group/read_only_int_value + 1.5" readonly="true()" />
<bind nodeset="/data/meta/instanceID" type="string" readonly="true()" jr:preload="uid" />
<bind nodeset="/data/group/geopoint-note" type="geopoint" readonly="true()" />
</model>
</h:head>
<h:body>
Expand Down Expand Up @@ -76,6 +78,9 @@
<input ref="/data/group/note_calc_decimal_from_int">
<label>A note with decimal type calculated from int</label>
</input>
<input ref="/data/group/geopoint-note">
<label>A note with geopoint type</label>
</input>
</group>
</h:body>
</h:html>
</h:html>
2 changes: 2 additions & 0 deletions packages/common/types/timers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type TimerID = ReturnType<typeof setTimeout>;
export type IntervalID = ReturnType<typeof setInterval>;
23 changes: 22 additions & 1 deletion packages/scenario/src/jr/event/InputQuestionEvent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import type { AnyInputNode, DecimalInputNode, IntInputNode } from '@getodk/xforms-engine';
import type {
AnyInputNode,
DecimalInputNode,
GeopointInputNode,
GeopointInputValue,
IntInputNode,
} from '@getodk/xforms-engine';
import { InputNodeAnswer } from '../../answer/InputNodeAnswer.ts';
import { UntypedAnswer } from '../../answer/UntypedAnswer.ts';
import type { ValueNodeAnswer } from '../../answer/ValueNodeAnswer.ts';
Expand Down Expand Up @@ -40,6 +46,18 @@ export class InputQuestionEvent extends QuestionEvent<'input'> {
}
}

private answerGeopointQuestionNode(
node: GeopointInputNode,
answerValue: unknown
): ValueNodeAnswer {
if (answerValue === null || typeof answerValue === 'object') {
node.setValue(answerValue as GeopointInputValue);
return new InputNodeAnswer(node);
}

return this.answerDefault(node, answerValue);
}

answerQuestion(answerValue: unknown): ValueNodeAnswer {
const { node } = this;

Expand All @@ -48,6 +66,9 @@ export class InputQuestionEvent extends QuestionEvent<'input'> {
case 'decimal':
return this.answerNumericQuestionNode(node, answerValue);

case 'geopoint':
return this.answerGeopointQuestionNode(node, answerValue);

default:
return this.answerDefault(node, answerValue);
}
Expand Down
140 changes: 138 additions & 2 deletions packages/scenario/test/bind-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ describe('Data (<bind type>) type support', () => {
t('implicit-string-value', 'implicit string'),
t('int-value', '123'),
t('decimal-value', '45.67'),
t('geopoint-value', '38.25146813817506 21.758421137528785 0 0'),
)
),
bind('/root/string-value').type('string').relevant(modelNodeRelevanceExpression),
bind('/root/implicit-string-value').relevant(modelNodeRelevanceExpression),
bind('/root/int-value').type('int').relevant(modelNodeRelevanceExpression),
bind('/root/decimal-value').type('decimal').relevant(modelNodeRelevanceExpression)
bind('/root/decimal-value').type('decimal').relevant(modelNodeRelevanceExpression),
bind('/root/geopoint-value').type('geopoint').relevant(modelNodeRelevanceExpression)
)
),
body(
Expand Down Expand Up @@ -182,6 +184,39 @@ describe('Data (<bind type>) type support', () => {
expect(answer.value).toBe(null);
});
});

describe('type="geopoint"', () => {
let answer: ModelValueNodeAnswer<'geopoint'>;

beforeEach(() => {
answer = getTypedModelValueNodeAnswer('/root/geopoint-value', 'geopoint');
});

it('has a (nullable) structured geopoint static type', () => {
interface ExpectedGeopointValue {
readonly latitude: number;
readonly longitude: number;
readonly altitude: number | null;
readonly accuracy: number | null;
}
expectTypeOf(answer.value).toEqualTypeOf<ExpectedGeopointValue | null>();
});

it('has a GeopointValue populated value', () => {
expect(answer.value).toEqual({
accuracy: 0,
altitude: 0,
latitude: 38.25146813817506,
longitude: 21.758421137528785,
});
});

it('has an null as blank value', () => {
scenario.answer(modelNodeRelevancePath, 'no');
answer = getTypedModelValueNodeAnswer('/root/geopoint-value', 'geopoint');
expect(answer.value).toBeNull();
});
});
});

describe('inputs', () => {
Expand All @@ -202,12 +237,14 @@ describe('Data (<bind type>) type support', () => {
t('implicit-string-value', 'implicit string'),
t('int-value', '123'),
t('decimal-value', '45.67'),
t('geopoint-value', '38.25146813817506 21.758421137528785 1000 25'),
)
),
bind('/root/string-value').type('string').relevant(inputRelevanceExpression),
bind('/root/implicit-string-value').relevant(inputRelevanceExpression),
bind('/root/int-value').type('int').relevant(inputRelevanceExpression),
bind('/root/decimal-value').type('decimal').relevant(inputRelevanceExpression)
bind('/root/decimal-value').type('decimal').relevant(inputRelevanceExpression),
bind('/root/geopoint-value').type('geopoint').relevant(inputRelevanceExpression)
)
),
body(
Expand All @@ -216,6 +253,7 @@ describe('Data (<bind type>) type support', () => {
input('/root/implicit-string-value'),
input('/root/int-value'),
input('/root/decimal-value'),
input('/root/geopoint-value'),
)
);

Expand Down Expand Up @@ -480,6 +518,104 @@ describe('Data (<bind type>) type support', () => {
});
});
});

describe('type="geopoint"', () => {
let answer: InputNodeAnswer<'geopoint'>;

beforeEach(() => {
answer = getTypedInputNodeAnswer('/root/geopoint-value', 'geopoint');
});

it('has a (nullable) structured geopoint static type', () => {
interface ExpectedGeopointValue {
readonly latitude: number;
readonly longitude: number;
readonly altitude: number | null;
readonly accuracy: number | null;
}
expectTypeOf(answer.value).toEqualTypeOf<ExpectedGeopointValue | null>();
});

it('has a GeopointValue populated value', () => {
expect(answer.value).toEqual({
latitude: 38.25146813817506,
longitude: 21.758421137528785,
altitude: 1000,
accuracy: 25,
});
expect(answer.stringValue).toEqual('38.25146813817506 21.758421137528785 1000 25');
});

it('has an null as blank value', () => {
scenario.answer(inputRelevancePath, 'no');
answer = getTypedInputNodeAnswer('/root/geopoint-value', 'geopoint');
expect(answer.value).toBeNull();
expect(answer.stringValue).toBe('');
});

it('sets altitude with value zero', () => {
scenario.answer('/root/geopoint-value', '-5.299 46.663 0 5');
answer = getTypedInputNodeAnswer('/root/geopoint-value', 'geopoint');
expect(answer.value).toEqual({
latitude: -5.299,
longitude: 46.663,
altitude: 0,
accuracy: 5,
});
expect(answer.stringValue).toEqual('-5.299 46.663 0 5');
});

it.each([
'ZYX %% ABC $$',
'ZYX %% 1200 10',
'-15.2936673 120.7260063 ABC $$',
'-2.33373 36.7260063 ABC 15',
'20.2936673 -16.7260063 1200 ABCD',
'99 179.99999 1200 0',
'89.999 180.1111 1300 0',
])('has null when incorrect value is passed', (expression) => {
scenario.answer('/root/geopoint-value', expression);
answer = getTypedInputNodeAnswer('/root/geopoint-value', 'geopoint');
expect(answer.value).toBeNull();
expect(answer.stringValue).toBe('');
});

it.each([
{
expression: { latitude: 20.663, longitude: 16.763 },
expectedAsObject: { latitude: 20.663, longitude: 16.763, altitude: null, accuracy: null },
expectedAsText: '20.663 16.763',
},
{
expression: { latitude: 19.899, longitude: 100.55559, accuracy: 15 },
expectedAsObject: { latitude: 19.899, longitude: 100.55559, altitude: 0, accuracy: 15 },
expectedAsText: '19.899 100.55559 0 15',
},
{
expression: { latitude: 45.111, longitude: 127.23, altitude: 1350 },
expectedAsObject: { latitude: 45.111, longitude: 127.23, altitude: 1350, accuracy: null },
expectedAsText: '45.111 127.23 1350',
},
{
expression: { latitude: 14.66599, longitude: 179.9009, altitude: 200, accuracy: 5 },
expectedAsObject: { latitude: 14.66599, longitude: 179.9009, altitude: 200, accuracy: 5 },
expectedAsText: '14.66599 179.9009 200 5',
},
{
expression: { latitude: 0, longitude: 0, altitude: 0, accuracy: 0 },
expectedAsObject: null,
expectedAsText: '',
},
])(
'sets value with GeopointValue object',
({ expression, expectedAsObject, expectedAsText }) => {
scenario.answer('/root/geopoint-value', expression);
answer = getTypedInputNodeAnswer('/root/geopoint-value', 'geopoint');
expect(answer.value).toEqual(expectedAsObject);
expect(answer.stringValue).toEqual(expectedAsText);
}
);
});
});

describe('casting fractional values to int', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
const currentPosition = this.getSelectedPositionalEvent();

if (currentPosition.eventType === 'END_OF_FORM') {
throw 'todo';

Check warning on line 105 in packages/scenario/test/smoketests/child-vaccination.test.ts

View workflow job for this annotation

GitHub Actions / Lint (global) (22.12.0)

Expected an error object to be thrown

Check warning on line 105 in packages/scenario/test/smoketests/child-vaccination.test.ts

View workflow job for this annotation

GitHub Actions / Lint (global) (22.12.0)

Expected an error object to be thrown
}

const events = this.getPositionalEvents();
Expand Down Expand Up @@ -138,7 +138,7 @@
const next = this.getNextEventPosition();

if (next.eventType === 'END_OF_FORM') {
throw 'todo';

Check warning on line 141 in packages/scenario/test/smoketests/child-vaccination.test.ts

View workflow job for this annotation

GitHub Actions / Lint (global) (22.12.0)

Expected an error object to be thrown

Check warning on line 141 in packages/scenario/test/smoketests/child-vaccination.test.ts

View workflow job for this annotation

GitHub Actions / Lint (global) (22.12.0)

Expected an error object to be thrown
}

return new JRTreeReference(next.node.currentState.reference);
Expand Down Expand Up @@ -760,7 +760,7 @@

scenario.next('/data/not_single');
scenario.next('/data/not_single/gps');
scenario.answer('1.234 5.678');
scenario.answer('1.234 5.678 0 2.3'); // an accuracy of 0m or greater than 5m makes a second geopoint question relevant
scenario.next('/data/building_name');
scenario.answer('Some building');
scenario.next('/data/full_address1');
Expand Down
Loading
Loading