Skip to content

Commit 6df2619

Browse files
committed
save new value and validations
1 parent db94e62 commit 6df2619

File tree

8 files changed

+192
-44
lines changed

8 files changed

+192
-44
lines changed

packages/web-forms/src/components/common/map/MapAdvancedPanel.vue

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import IconSVG from '@/components/common/IconSVG.vue';
3+
import { toGeoJsonCoordinateArray } from '@/components/common/map/map-helpers.ts';
34
import { fromLonLat, toLonLat } from 'ol/proj';
45
import { ref, watch } from 'vue';
56
import type { Coordinate } from 'ol/coordinate';
@@ -32,26 +33,19 @@ watch(
3233
);
3334
3435
const updateVertex = () => {
35-
const [originalLong, originalLat] = props.selectedVertex ?? [];
36-
if (!longitude.value) {
37-
longitude.value = originalLong;
36+
const [originalLong, originalLat] = toLonLat(props.selectedVertex ?? []);
37+
const long = longitude.value ?? originalLong;
38+
const lat = latitude.value ?? originalLat;
39+
if (long === undefined || lat === undefined) {
3840
return;
3941
}
4042
41-
if (!latitude.value) {
42-
latitude.value = originalLat;
43-
return;
44-
}
45-
46-
const newVertex = [longitude.value, latitude.value];
47-
if (altitude.value != null) {
48-
newVertex.push(altitude.value);
49-
}
50-
51-
if (accuracy.value != null) {
52-
newVertex.push(accuracy.value);
53-
}
54-
43+
const newVertex = toGeoJsonCoordinateArray(
44+
long,
45+
lat,
46+
altitude.value,
47+
accuracy.value
48+
) as Coordinate;
5549
emit('save', fromLonLat(newVertex));
5650
};
5751
</script>

packages/web-forms/src/components/common/map/MapBlock.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,14 @@ const undoLastChange = () => {
141141
emitSavedFeature();
142142
};
143143
144-
const updateFeatureCoords = () => {
145-
// todo
144+
const updateFeatureCoords = (newCoords: Coordinate[] & Coordinate[][]) => {
145+
mapHandler.updateFeatureCoordinates(newCoords);
146+
emitSavedFeature();
146147
};
147148
148-
const updateVertexCoords = () => {
149-
// todo
149+
const updateVertexCoords = (newCoords: Coordinate) => {
150+
mapHandler.updateVertexCoords(newCoords);
151+
emitSavedFeature();
150152
};
151153
</script>
152154

packages/web-forms/src/components/common/map/MapUpdateCoordsDialog.vue

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
<script setup lang="ts">
22
import { getGeoJSONCoordinates } from '@/components/common/map/createFeatureCollectionAndProps.ts';
3-
import type { DrawFeatureType } from '@/components/common/map/useMapInteractions.ts';
3+
import {
4+
DRAW_FEATURE_TYPES,
5+
type DrawFeatureType,
6+
} from '@/components/common/map/useMapInteractions.ts';
7+
import { isCoordsEqual } from '@/components/common/map/vertex-geometry.ts';
48
import type { FeatureCollection, LineString, Point, Polygon } from 'geojson';
9+
import { fromLonLat } from 'ol/proj';
510
import Button from 'primevue/button';
611
import Dialog from 'primevue/dialog';
712
import InputText from 'primevue/inputtext';
813
import IconSVG from '@/components/common/IconSVG.vue';
914
import { ref, computed, watch } from 'vue';
1015
import type { Coordinate } from 'ol/coordinate';
1116
12-
defineProps<{
17+
const props = defineProps<{
1318
visible: boolean;
1419
drawFeatureType?: DrawFeatureType;
1520
}>();
@@ -42,6 +47,7 @@ const parseFileCoordinates = async (file: File): Promise<Coordinate[] | undefine
4247
try {
4348
const text = await file.text();
4449
if (!text.trim()) {
50+
// TODO: translations
4551
error.value = 'File is empty.';
4652
return;
4753
}
@@ -54,9 +60,10 @@ const parseFileCoordinates = async (file: File): Promise<Coordinate[] | undefine
5460
if (fileName.endsWith('.csv')) {
5561
return parseCSVGeometry(text);
5662
}
57-
63+
// TODO: translations
5864
error.value = 'Unsupported file type. Please upload a .csv or .geojson file.';
5965
} catch {
66+
// TODO: translations
6067
error.value = 'Failed to parse file. Ensure it is valid CSV or GeoJSON.';
6168
}
6269
};
@@ -97,6 +104,26 @@ const parsePastedValue = () => {
97104
return getGeoJSONCoordinates(value);
98105
};
99106
107+
const isExpectedFeatureType = (coords: Coordinate | Coordinate[] | Coordinate[][]) => {
108+
const isPoint = !props.drawFeatureType && !Array.isArray(coords[0]) && coords.length > 2;
109+
if (isPoint) {
110+
return true;
111+
}
112+
113+
const hasRing = Array.isArray(coords[0]) && Array.isArray(coords[0][0]);
114+
const flatCoords = (hasRing ? coords[0] : coords) as Coordinate[];
115+
if (!flatCoords?.length) {
116+
return false;
117+
}
118+
119+
const isClosed = isCoordsEqual(flatCoords[0], flatCoords[flatCoords.length - 1]);
120+
if (props.drawFeatureType === DRAW_FEATURE_TYPES.TRACE && !isClosed && flatCoords.length >= 2) {
121+
return true;
122+
}
123+
124+
return props.drawFeatureType === DRAW_FEATURE_TYPES.SHAPE && isClosed && flatCoords.length >= 3;
125+
};
126+
100127
const save = async () => {
101128
error.value = null;
102129
let coordinates: Coordinate[] | undefined;
@@ -107,10 +134,18 @@ const save = async () => {
107134
}
108135
109136
if (!coordinates?.length) {
137+
// TODO: translations
110138
error.value ??= 'No valid coordinates found.';
111139
return;
112140
}
113141
142+
coordinates = coordinates.map((coord) => fromLonLat(coord));
143+
if (!isExpectedFeatureType(coordinates)) {
144+
// TODO: translations
145+
error.value ??= 'Incorrect geometry type.';
146+
return;
147+
}
148+
114149
emit('save', coordinates);
115150
close();
116151
};
@@ -149,19 +184,23 @@ watch(pasteValue, (newVal) => {
149184
@update:visible="emit('update:visible', $event)"
150185
>
151186
<template #header>
187+
<!-- TODO: translations -->
152188
<strong>Paste or upload location data</strong>
153189
</template>
154190

155191
<template #default>
156192
<div class="dialog-field-container">
193+
<!-- TODO: translations -->
157194
<label for="paste-input">Paste the new value in ODK format</label>
158195
<InputText id="paste-input" v-model="pasteValue" />
159196
</div>
160197

161198
<div class="dialog-field-container">
199+
<!-- TODO: translations -->
162200
<label>Or upload a GeoJSON or a CSV file</label>
163201
<Button outlined severity="contrast" @click="openFileChooser">
164202
<IconSVG name="mdiUpload" />
203+
<!-- TODO: translations -->
165204
<span>Upload file</span>
166205
</Button>
167206

@@ -170,11 +209,12 @@ watch(pasteValue, (newVal) => {
170209
type="file"
171210
accept=".geojson,.csv,application/json,text/csv"
172211
@change="selectFile"
173-
>
212+
/>
174213
</div>
175214
</template>
176215

177216
<template #footer>
217+
<p v-if="error?.length" class="coords-error-message">{{ error }}</p>
178218
<Button label="Save" :disabled="!selectedFile && !hasPastedValue" @click="save" />
179219
</template>
180220
</Dialog>
@@ -201,6 +241,11 @@ watch(pasteValue, (newVal) => {
201241
padding: 9px;
202242
}
203243
}
244+
245+
.coords-error-message {
246+
color: var(--odk-error-text-color);
247+
margin-bottom: 10px;
248+
}
204249
</style>
205250

206251
<style lang="scss">

packages/web-forms/src/components/common/map/createFeatureCollectionAndProps.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { toGeoJsonCoordinateArray } from '@/components/common/map/map-helpers.ts';
12
import type { SelectItem } from '@getodk/xforms-engine';
23

34
const PROPERTY_PREFIX = 'odk_'; // Avoids conflicts with OpenLayers (for example, geometry).
@@ -30,25 +31,33 @@ export const getGeoJSONCoordinates = (
3031
): [Coordinates, ...Coordinates[]] | undefined => {
3132
const coordinates: Coordinates[] = [];
3233
for (const coord of geometry.split(';')) {
33-
const [lat, lon] = coord.trim().split(/\s+/).map(Number);
34+
const [lat, lon, alt, acc] = coord.trim().split(/\s+/).map(Number);
3435

3536
const isNullLocation = lat === 0 && lon === 0;
3637
const isValidLatitude = lat != null && !Number.isNaN(lat) && Math.abs(lat) <= 90;
3738
const isValidLongitude = lon != null && !Number.isNaN(lon) && Math.abs(lon) <= 180;
39+
const isAltitudeProvided = alt != null && !Number.isNaN(alt);
40+
const isAccuracyProvided = acc != null && !Number.isNaN(acc);
3841

3942
if (isNullLocation || !isValidLatitude || !isValidLongitude) {
4043
// eslint-disable-next-line no-console -- Skip silently to match Collect behaviour.
4144
console.warn(`Invalid geo point coordinates: ${geometry}`);
4245
return;
4346
}
4447

45-
coordinates.push([lon, lat]);
48+
const parsedCoords = toGeoJsonCoordinateArray(
49+
lon,
50+
lat,
51+
isAltitudeProvided ? alt : undefined,
52+
isAccuracyProvided ? acc : undefined
53+
) as Coordinates;
54+
coordinates.push(parsedCoords);
4655
}
4756

4857
return coordinates.length ? (coordinates as [Coordinates, ...Coordinates[]]) : undefined;
4958
};
5059

51-
const getGeoJSONGeometry = (coords: [Coordinates, ...Coordinates[]]): Geometry => {
60+
export const getGeoJSONGeometry = (coords: [Coordinates, ...Coordinates[]]): Geometry => {
5261
if (coords.length === 1) {
5362
return { type: 'Point', coordinates: coords[0] };
5463
}

packages/web-forms/src/components/common/map/map-helpers.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,52 @@ import type Feature from 'ol/Feature';
44
import type { LineString, Point, Polygon } from 'ol/geom';
55
import { toLonLat } from 'ol/proj';
66

7+
const resolveAltitudeAndAccuracy = (
8+
altitude: number | null | undefined,
9+
accuracy: number | null | undefined
10+
) => {
11+
const arr = [];
12+
if (accuracy != null) {
13+
arr.push(altitude ?? 0, accuracy);
14+
} else if (altitude != null) {
15+
arr.push(altitude);
16+
}
17+
return arr;
18+
};
19+
20+
// Longitude is first for GeoJSON and latitude is second.
21+
export const toGeoJsonCoordinateArray = (
22+
longitude: number,
23+
latitude: number,
24+
altitude: number | null | undefined,
25+
accuracy: number | null | undefined
26+
): number[] => {
27+
const coords = [longitude, latitude];
28+
coords.push(...resolveAltitudeAndAccuracy(altitude, accuracy));
29+
return coords;
30+
};
31+
32+
// Latitude is first for ODK and longitude is second.
33+
export const toODKCoordinateArray = (
34+
longitude: number,
35+
latitude: number,
36+
altitude: number | null | undefined,
37+
accuracy: number | null | undefined
38+
): number[] => {
39+
const coords = [latitude, longitude];
40+
coords.push(...resolveAltitudeAndAccuracy(altitude, accuracy));
41+
return coords;
42+
};
43+
744
export const formatODKValue = (feature: Feature): string => {
845
const geometry = feature.getGeometry();
946
if (!geometry) {
1047
return '';
1148
}
1249

1350
const formatCoords = (coords: Coordinate) => {
14-
const [longitude, latitude, altitude, accuracy] = toLonLat(coords);
15-
return [latitude, longitude, altitude, accuracy].filter((item) => item != null).join(' ');
51+
const parsedCoords = toLonLat(coords) as [number, number, number?, number?];
52+
return toODKCoordinateArray(...parsedCoords).join(' ');
1653
};
1754

1855
const featureType = geometry.getType();

0 commit comments

Comments
 (0)