Skip to content

Commit e24b60b

Browse files
committed
Adds support for editing point feature
1 parent 6df2619 commit e24b60b

File tree

8 files changed

+131
-78
lines changed

8 files changed

+131
-78
lines changed

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<script setup lang="ts">
22
import IconSVG from '@/components/common/IconSVG.vue';
33
import { toGeoJsonCoordinateArray } from '@/components/common/map/map-helpers.ts';
4-
import { fromLonLat, toLonLat } from 'ol/proj';
4+
import { fromLonLat } from 'ol/proj';
55
import { ref, watch } from 'vue';
66
import type { Coordinate } from 'ol/coordinate';
77
88
const props = defineProps<{
99
isOpen: boolean;
10-
selectedVertex: Coordinate | undefined;
10+
coordinates: Coordinate | undefined;
1111
}>();
1212
1313
const emit = defineEmits(['open-paste-dialog', 'save']);
@@ -18,22 +18,26 @@ const altitude = ref<number | undefined>();
1818
const longitude = ref<number | undefined>();
1919
2020
watch(
21-
() => props.selectedVertex,
21+
() => props.coordinates,
2222
(newVal) => {
2323
if (newVal) {
24-
[longitude.value, latitude.value, altitude.value, accuracy.value] = toLonLat(newVal);
24+
[longitude.value, latitude.value, altitude.value, accuracy.value] = newVal;
2525
return;
2626
}
2727
accuracy.value = undefined;
2828
latitude.value = undefined;
2929
altitude.value = undefined;
3030
longitude.value = undefined;
3131
},
32-
{ deep: true }
32+
{ immediate: true }
3333
);
3434
3535
const updateVertex = () => {
36-
const [originalLong, originalLat] = toLonLat(props.selectedVertex ?? []);
36+
if (!props.coordinates?.length) {
37+
return;
38+
}
39+
40+
const [originalLong, originalLat] = props.coordinates;
3741
const long = longitude.value ?? originalLong;
3842
const lat = latitude.value ?? originalLat;
3943
if (long === undefined || lat === undefined) {

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

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import MapUpdateCoordsDialog from '@/components/common/map/MapUpdateCoordsDialog
1515
import { STATES, useMapBlock } from '@/components/common/map/useMapBlock.ts';
1616
import { type DrawFeatureType } from '@/components/common/map/useMapInteractions.ts';
1717
import { QUESTION_HAS_ERROR } from '@/lib/constants/injection-keys.ts';
18-
import type { Feature, FeatureCollection } from 'geojson';
18+
import type { Feature, FeatureCollection, Point as PointGeoJSON } from 'geojson';
1919
import type { Coordinate } from 'ol/coordinate';
20+
import { toLonLat } from 'ol/proj';
2021
import Button from 'primevue/button';
2122
import Message from 'primevue/message';
2223
import { computed, type ComputedRef, inject, onMounted, onUnmounted, ref, watch } from 'vue';
@@ -56,6 +57,20 @@ const mapHandler = useMapBlock(
5657
}
5758
);
5859
60+
const advancedPanelCoords = computed<Coordinate | undefined>(() => {
61+
// if !can-open-advanced-panel return
62+
if (props.drawFeatureType && selectedVertex.value) {
63+
return toLonLat(selectedVertex.value);
64+
}
65+
66+
const geometry = props.savedFeatureValue?.geometry as PointGeoJSON | undefined;
67+
if (geometry) {
68+
return geometry.coordinates;
69+
}
70+
71+
return undefined;
72+
});
73+
5974
const showSecondaryControls = computed(() => {
6075
return !props.disabled && (mapHandler.canUndoChange() || mapHandler.canDeleteFeatureOrVertex());
6176
});
@@ -141,14 +156,19 @@ const undoLastChange = () => {
141156
emitSavedFeature();
142157
};
143158
144-
const updateFeatureCoords = (newCoords: Coordinate[] & Coordinate[][]) => {
159+
const updateFeatureCoords = (newCoords: Coordinate | Coordinate[] | Coordinate[][]) => {
145160
mapHandler.updateFeatureCoordinates(newCoords);
146161
emitSavedFeature();
147162
};
148163
149-
const updateVertexCoords = (newCoords: Coordinate) => {
150-
mapHandler.updateVertexCoords(newCoords);
151-
emitSavedFeature();
164+
const saveAdvancedPanelCoords = (newCoords: Coordinate) => {
165+
if (props.drawFeatureType) {
166+
mapHandler.updateVertexCoords(newCoords);
167+
emitSavedFeature();
168+
return;
169+
}
170+
171+
updateFeatureCoords(newCoords);
152172
};
153173
</script>
154174

@@ -191,6 +211,7 @@ const updateVertexCoords = (newCoords: Coordinate) => {
191211
</div>
192212

193213
<MapStatusBar
214+
:can-enable-advanced-panel="!!advancedPanelCoords"
194215
:can-open-advanced-panel="true"
195216
:can-remove="!disabled && mapHandler.canRemoveCurrentLocation()"
196217
:can-save="!disabled && mapHandler.canSaveCurrentLocation()"
@@ -208,9 +229,9 @@ const updateVertexCoords = (newCoords: Coordinate) => {
208229

209230
<MapAdvancedPanel
210231
:is-open="isAdvancedPanelOpen"
211-
:selected-vertex="selectedVertex"
232+
:coordinates="advancedPanelCoords"
212233
@open-paste-dialog="isUpdateCoordsDialogOpen = true"
213-
@save="updateVertexCoords"
234+
@save="saveAdvancedPanelCoords"
214235
/>
215236

216237
<MapProperties

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

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const props = defineProps<{
2828
canSave: boolean;
2929
canViewDetails: boolean;
3030
canOpenAdvancedPanel: boolean;
31+
canEnableAdvancedPanel: boolean;
3132
}>();
3233
3334
const emit = defineEmits(['discard', 'open-advanced-panel', 'save', 'view-details']);
@@ -129,21 +130,23 @@ const savedStatus = computed<StatusDetails | null>(() => {
129130
<IconSVG :name="savedStatus.icon" :variant="savedStatus.highlight ? 'success' : 'base'" />
130131
<span>{{ savedStatus.message }}</span>
131132
</div>
132-
<Button v-if="canRemove" outlined severity="contrast" @click="emit('discard')">
133-
<span>–</span>
134-
<!-- TODO: translations -->
135-
<span class="mobile-only">Remove</span>
136-
<span class="desktop-only">Remove point</span>
137-
</Button>
138-
<Button v-if="canViewDetails" outlined severity="contrast" @click="emit('view-details')">
139-
<!-- TODO: translations -->
140-
<span>View details</span>
141-
</Button>
142-
<Button v-if="canOpenAdvancedPanel" class="advanced-button" :disabled="!selectedVertexInfo.length" outlined severity="contrast" @click="emit('open-advanced-panel')">
143-
<IconSVG name="mdiCogOutline" />
144-
<!-- TODO: translations -->
145-
<span>Advanced</span>
146-
</Button>
133+
<div class="map-status-buttons">
134+
<Button v-if="canRemove" outlined severity="contrast" @click="emit('discard')">
135+
<span>–</span>
136+
<!-- TODO: translations -->
137+
<span class="mobile-only">Remove</span>
138+
<span class="desktop-only">Remove point</span>
139+
</Button>
140+
<Button v-if="canViewDetails" outlined severity="contrast" @click="emit('view-details')">
141+
<!-- TODO: translations -->
142+
<span>View details</span>
143+
</Button>
144+
<Button v-if="canOpenAdvancedPanel" class="advanced-button" :disabled="!canEnableAdvancedPanel" outlined severity="contrast" @click="emit('open-advanced-panel')">
145+
<IconSVG name="mdiCogOutline" />
146+
<!-- TODO: translations -->
147+
<span>Advanced</span>
148+
</Button>
149+
</div>
147150
</div>
148151

149152
<div v-else class="map-status-container">
@@ -207,6 +210,11 @@ const savedStatus = computed<StatusDetails | null>(() => {
207210
height: 20px;
208211
}
209212
213+
.map-status-buttons {
214+
display: flex;
215+
gap: var(--odk-map-controls-spacing);
216+
}
217+
210218
@media screen and (max-width: #{pf.$sm}) {
211219
.advanced-button span {
212220
display: none;

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

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<script setup lang="ts">
2-
import { getGeoJSONCoordinates } from '@/components/common/map/createFeatureCollectionAndProps.ts';
2+
import { createGeoJSONGeometry } from '@/components/common/map/createFeatureCollectionAndProps.ts';
33
import {
44
DRAW_FEATURE_TYPES,
55
type DrawFeatureType,
66
} from '@/components/common/map/useMapInteractions.ts';
77
import { isCoordsEqual } from '@/components/common/map/vertex-geometry.ts';
8-
import type { FeatureCollection, LineString, Point, Polygon } from 'geojson';
8+
import type { FeatureCollection, Geometry, LineString, Point, Polygon } from 'geojson';
99
import { fromLonLat } from 'ol/proj';
1010
import Button from 'primevue/button';
1111
import Dialog from 'primevue/dialog';
@@ -43,7 +43,7 @@ const selectFile = (event: Event) => {
4343
error.value = null;
4444
};
4545
46-
const parseFileCoordinates = async (file: File): Promise<Coordinate[] | undefined> => {
46+
const parseFileCoordinates = async (file: File): Promise<Geometry | undefined> => {
4747
try {
4848
const text = await file.text();
4949
if (!text.trim()) {
@@ -68,17 +68,12 @@ const parseFileCoordinates = async (file: File): Promise<Coordinate[] | undefine
6868
}
6969
};
7070
71-
const parseGeoJSONCoordinates = (text: string): Coordinate[] | undefined => {
71+
const parseGeoJSONCoordinates = (text: string): Geometry | undefined => {
7272
const geojson = JSON.parse(text) as FeatureCollection<LineString | Point | Polygon>;
73-
const coords = geojson?.features?.[0]?.geometry?.coordinates as Coordinate[] | undefined;
74-
if (!Array.isArray(coords)) {
75-
return;
76-
}
77-
78-
return coords;
73+
return geojson?.features?.[0]?.geometry;
7974
};
8075
81-
const parseCSVGeometry = (text: string): Coordinate[] | undefined => {
76+
const parseCSVGeometry = (text: string): Geometry | undefined => {
8277
const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0);
8378
if (lines.length < 2) {
8479
return;
@@ -92,7 +87,7 @@ const parseCSVGeometry = (text: string): Coordinate[] | undefined => {
9287
9388
const firstDataRow = lines[1]?.split(',') ?? [];
9489
const geometryValue = firstDataRow[geometryIndex]?.trim() ?? '';
95-
return getGeoJSONCoordinates(geometryValue);
90+
return createGeoJSONGeometry(geometryValue) as Geometry | undefined;
9691
};
9792
9893
const parsePastedValue = () => {
@@ -101,58 +96,69 @@ const parsePastedValue = () => {
10196
return;
10297
}
10398
104-
return getGeoJSONCoordinates(value);
99+
return createGeoJSONGeometry(value) as Geometry | undefined;
105100
};
106101
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;
102+
const getValidCoordinates = (geometry: LineString | Point | Polygon | undefined) => {
103+
if (!geometry?.coordinates) {
104+
return;
105+
}
106+
107+
const coords = geometry.coordinates as Coordinate | Coordinate[] | Coordinate[][];
108+
if (geometry.type === 'Point' && !props.drawFeatureType && !Array.isArray(coords[0])) {
109+
return fromLonLat(coords as Coordinate);
111110
}
112111
113112
const hasRing = Array.isArray(coords[0]) && Array.isArray(coords[0][0]);
114-
const flatCoords = (hasRing ? coords[0] : coords) as Coordinate[];
113+
let flatCoords = (hasRing ? coords[0] : coords) as Coordinate[];
115114
if (!flatCoords?.length) {
116-
return false;
115+
return;
117116
}
118117
118+
flatCoords = flatCoords.map((c) => fromLonLat(c));
119119
const isClosed = isCoordsEqual(flatCoords[0], flatCoords[flatCoords.length - 1]);
120-
if (props.drawFeatureType === DRAW_FEATURE_TYPES.TRACE && !isClosed && flatCoords.length >= 2) {
121-
return true;
120+
if (
121+
geometry.type === 'LineString' &&
122+
props.drawFeatureType === DRAW_FEATURE_TYPES.TRACE &&
123+
!isClosed &&
124+
flatCoords.length >= 2
125+
) {
126+
return flatCoords;
122127
}
123128
124-
return props.drawFeatureType === DRAW_FEATURE_TYPES.SHAPE && isClosed && flatCoords.length >= 3;
129+
if (
130+
geometry.type === 'Polygon' &&
131+
props.drawFeatureType === DRAW_FEATURE_TYPES.SHAPE &&
132+
isClosed &&
133+
flatCoords.length >= 3
134+
) {
135+
return [flatCoords];
136+
}
125137
};
126138
127139
const save = async () => {
128140
error.value = null;
129-
let coordinates: Coordinate[] | undefined;
141+
let geometry;
130142
if (selectedFile.value) {
131-
coordinates = await parseFileCoordinates(selectedFile.value);
143+
geometry = await parseFileCoordinates(selectedFile.value);
132144
} else if (hasPastedValue.value) {
133-
coordinates = parsePastedValue();
145+
geometry = parsePastedValue();
134146
}
135147
148+
const coordinates = getValidCoordinates(geometry as LineString | Point | Polygon | undefined);
136149
if (!coordinates?.length) {
137-
// TODO: translations
138-
error.value ??= 'No valid coordinates found.';
139-
return;
140-
}
141-
142-
coordinates = coordinates.map((coord) => fromLonLat(coord));
143-
if (!isExpectedFeatureType(coordinates)) {
144150
// TODO: translations
145151
error.value ??= 'Incorrect geometry type.';
146152
return;
147153
}
148154
149-
emit('save', coordinates);
150155
close();
156+
emit('save', coordinates);
151157
};
152158
153159
const close = () => {
154-
emit('update:visible', false);
155160
reset();
161+
emit('update:visible', false);
156162
};
157163
158164
const reset = () => {
@@ -182,6 +188,7 @@ watch(pasteValue, (newVal) => {
182188
class="map-paste-dialog"
183189
:draggable="false"
184190
@update:visible="emit('update:visible', $event)"
191+
@after-hide="reset"
185192
>
186193
<template #header>
187194
<!-- TODO: translations -->
@@ -198,6 +205,8 @@ watch(pasteValue, (newVal) => {
198205
<div class="dialog-field-container">
199206
<!-- TODO: translations -->
200207
<label>Or upload a GeoJSON or a CSV file</label>
208+
<!-- TODO: translations -->
209+
<span v-if="selectedFile"><i>File uploaded</i></span>
201210
<Button outlined severity="contrast" @click="openFileChooser">
202211
<IconSVG name="mdiUpload" />
203212
<!-- TODO: translations -->
@@ -209,12 +218,14 @@ watch(pasteValue, (newVal) => {
209218
type="file"
210219
accept=".geojson,.csv,application/json,text/csv"
211220
@change="selectFile"
212-
/>
221+
>
213222
</div>
214223
</template>
215224

216225
<template #footer>
217-
<p v-if="error?.length" class="coords-error-message">{{ error }}</p>
226+
<p v-if="error?.length" class="coords-error-message">
227+
{{ error }}
228+
</p>
218229
<Button label="Save" :disabled="!selectedFile && !hasPastedValue" @click="save" />
219230
</template>
220231
</Dialog>
@@ -243,6 +254,7 @@ watch(pasteValue, (newVal) => {
243254
}
244255
245256
.coords-error-message {
257+
display: block;
246258
color: var(--odk-error-text-color);
247259
margin-bottom: 10px;
248260
}

0 commit comments

Comments
 (0)