diff --git a/.changeset/young-nails-type.md b/.changeset/young-nails-type.md new file mode 100644 index 000000000..f17d12374 --- /dev/null +++ b/.changeset/young-nails-type.md @@ -0,0 +1,5 @@ +--- +'@getodk/web-forms': minor +--- + +Adds support for editing the coordinates in geoshape and geotrace question types. diff --git a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-autoclose-polygon-chromium.png b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-autoclose-polygon-chromium.png index 5e654b6cf..e8b15756f 100644 Binary files a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-autoclose-polygon-chromium.png and b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-autoclose-polygon-chromium.png differ diff --git a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-autoclose-polygon-firefox.png b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-autoclose-polygon-firefox.png index 670cc04fc..a304b6cc5 100644 Binary files a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-autoclose-polygon-firefox.png and b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-autoclose-polygon-firefox.png differ diff --git a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-autoclose-polygon-webkit.png b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-autoclose-polygon-webkit.png index d1896e73b..28ce08d7e 100644 Binary files a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-autoclose-polygon-webkit.png and b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-autoclose-polygon-webkit.png differ diff --git a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-three-points-chromium.png b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-three-points-chromium.png index c6dd5a86d..c7bf1ee90 100644 Binary files a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-three-points-chromium.png and b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-three-points-chromium.png differ diff --git a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-three-points-firefox.png b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-three-points-firefox.png index af48b2b8f..4cf6017b2 100644 Binary files a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-three-points-firefox.png and b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-three-points-firefox.png differ diff --git a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-three-points-webkit.png b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-three-points-webkit.png index 6efcfea7f..36afbcc5c 100644 Binary files a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-three-points-webkit.png and b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geoshape-three-points-webkit.png differ diff --git a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geotrace-two-points-chromium.png b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geotrace-two-points-chromium.png index ce75bbd31..c6e460a8e 100644 Binary files a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geotrace-two-points-chromium.png and b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geotrace-two-points-chromium.png differ diff --git a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geotrace-two-points-firefox.png b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geotrace-two-points-firefox.png index c021836f8..2d6576667 100644 Binary files a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geotrace-two-points-firefox.png and b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geotrace-two-points-firefox.png differ diff --git a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geotrace-two-points-webkit.png b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geotrace-two-points-webkit.png index 244b060df..e07f025ed 100644 Binary files a/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geotrace-two-points-webkit.png and b/packages/web-forms/e2e/test-cases/visual/all-question-types.test.ts-snapshots/geotrace-two-points-webkit.png differ diff --git a/packages/web-forms/src/components/common/IconSVG.vue b/packages/web-forms/src/components/common/IconSVG.vue index c44935775..1d4fe26ab 100644 --- a/packages/web-forms/src/components/common/IconSVG.vue +++ b/packages/web-forms/src/components/common/IconSVG.vue @@ -12,6 +12,7 @@ import { mdiChevronDown, mdiChevronUp, mdiClose, + mdiCogOutline, mdiContentSave, mdiCrosshairsGps, mdiDotsVertical, @@ -48,6 +49,7 @@ const iconMap: Record = { mdiChevronDown, mdiChevronUp, mdiClose, + mdiCogOutline, mdiContentSave, mdiCrosshairsGps, mdiDotsVertical, diff --git a/packages/web-forms/src/components/common/map/AsyncMap.vue b/packages/web-forms/src/components/common/map/AsyncMap.vue index 1b29067f2..bcec021d1 100644 --- a/packages/web-forms/src/components/common/map/AsyncMap.vue +++ b/packages/web-forms/src/components/common/map/AsyncMap.vue @@ -4,21 +4,17 @@ * This prevents unnecessary bloat in the main application bundle, reducing initial load times and improving performance. * Use dynamic imports instead (e.g., `await import(importPath)`) for lazy-loading these dependencies only when required. */ +import { createFeatureCollectionAndProps } from '@/components/common/map/geojson-parsers.ts'; +import type { Mode, SingleFeatureType } from '@/components/common/map/getModeConfig.ts'; import type { SelectItem } from '@getodk/xforms-engine'; +import type { Feature } from 'geojson'; import ProgressSpinner from 'primevue/progressspinner'; import { computed, type DefineComponent, onMounted, shallowRef } from 'vue'; -import type { Mode } from '@/components/common/map/getModeConfig.ts'; -import { - createFeatureCollectionAndProps, - type Feature, -} from '@/components/common/map/createFeatureCollectionAndProps.ts'; - -type DrawFeatureType = 'shape' | 'trace'; type MapBlockComponent = DefineComponent<{ featureCollection: { type: string; features: Feature[] }; disabled: boolean; - drawFeatureType?: DrawFeatureType; + singleFeatureType?: SingleFeatureType; mode: Mode; orderedExtraProps: Map>; savedFeatureValue: Feature | undefined; @@ -27,7 +23,7 @@ type MapBlockComponent = DefineComponent<{ interface AsyncMapProps { features?: readonly SelectItem[]; disabled: boolean; - drawFeatureType?: DrawFeatureType; + singleFeatureType?: SingleFeatureType; mode: Mode; savedFeatureValue: string | undefined; } @@ -86,7 +82,7 @@ onMounted(loadMap); +import IconSVG from '@/components/common/IconSVG.vue'; +import { + isNullLocation, + isValidLatitude, + isValidLongitude, + toGeoJsonCoordinateArray, +} from '@/components/common/map/geojson-parsers.ts'; +import { fromLonLat } from 'ol/proj'; +import { computed, ref, watch } from 'vue'; +import type { Coordinate } from 'ol/coordinate'; + +const props = defineProps<{ + isOpen: boolean; + coordinates: Coordinate | null; +}>(); + +const emit = defineEmits(['open-paste-dialog', 'save']); + +const latitude = ref(); +const longitude = ref(); +const accuracy = ref(); +const altitude = ref(); +const disableInputs = computed(() => !props.coordinates?.length); +const validLatitude = computed(() => { + return isValidLatitude(latitude.value) && !isNullLocation(latitude.value, longitude.value); +}); +const validLongitude = computed(() => { + return isValidLongitude(longitude.value) && !isNullLocation(latitude.value, longitude.value); +}); + +watch( + () => props.coordinates, + (newVal) => { + if (newVal) { + [longitude.value, latitude.value, altitude.value, accuracy.value] = newVal; + return; + } + accuracy.value = undefined; + latitude.value = undefined; + altitude.value = undefined; + longitude.value = undefined; + }, + { immediate: true } +); + +const updateVertex = () => { + if (!validLatitude.value || !validLongitude.value) { + return; + } + + const newVertex = toGeoJsonCoordinateArray( + Number(longitude.value), + Number(latitude.value), + Number(altitude.value), + Number(accuracy.value) + ) as Coordinate; + + if (newVertex.length) { + emit('save', fromLonLat(newVertex)); + } +}; + + + + + diff --git a/packages/web-forms/src/components/common/map/MapBlock.vue b/packages/web-forms/src/components/common/map/MapBlock.vue index 712658882..eecdd4925 100644 --- a/packages/web-forms/src/components/common/map/MapBlock.vue +++ b/packages/web-forms/src/components/common/map/MapBlock.vue @@ -5,16 +5,18 @@ * load on demand. Avoids main bundle bloat. */ import IconSVG from '@/components/common/IconSVG.vue'; -import type { Mode } from '@/components/common/map/getModeConfig.ts'; -import MapConfirm from '@/components/common/map/MapConfirm.vue'; +import { type Mode, type SingleFeatureType } from '@/components/common/map/getModeConfig.ts'; +import MapAdvancedPanel from '@/components/common/map/MapAdvancedPanel.vue'; +import MapConfirmDialog from '@/components/common/map/MapConfirmDialog.vue'; import MapControls from '@/components/common/map/MapControls.vue'; import MapProperties from '@/components/common/map/MapProperties.vue'; import MapStatusBar from '@/components/common/map/MapStatusBar.vue'; +import MapUpdateCoordsDialog from '@/components/common/map/MapUpdateCoordsDialog.vue'; import { STATES, useMapBlock } from '@/components/common/map/useMapBlock.ts'; -import { type DrawFeatureType } from '@/components/common/map/useMapInteractions.ts'; import { QUESTION_HAS_ERROR } from '@/lib/constants/injection-keys.ts'; import type { Feature, FeatureCollection } from 'geojson'; import type { Coordinate } from 'ol/coordinate'; +import { toLonLat } from 'ol/proj'; import Button from 'primevue/button'; import Message from 'primevue/message'; import { computed, type ComputedRef, inject, onMounted, onUnmounted, ref, watch } from 'vue'; @@ -22,7 +24,7 @@ import { computed, type ComputedRef, inject, onMounted, onUnmounted, ref, watch interface MapBlockProps { featureCollection: FeatureCollection; disabled: boolean; - drawFeatureType?: DrawFeatureType; + singleFeatureType?: SingleFeatureType; mode: Mode; orderedExtraProps: Map>; savedFeatureValue: Feature | undefined; @@ -32,7 +34,9 @@ const props = defineProps(); const emit = defineEmits(['save']); const mapElement = ref(); const isFullScreen = ref(false); +const isAdvancedPanelOpen = ref(false); const confirmDeleteAction = ref(false); +const isUpdateCoordsDialogOpen = ref(false); const selectedVertex = ref(); const showErrorStyle = inject>( QUESTION_HAS_ERROR, @@ -40,13 +44,20 @@ const showErrorStyle = inject>( ); const mapHandler = useMapBlock( - { mode: props.mode, drawFeatureType: props.drawFeatureType }, + { mode: props.mode, singleFeatureType: props.singleFeatureType }, { onFeaturePlacement: () => emitSavedFeature(), onVertexSelect: (vertex) => (selectedVertex.value = vertex), } ); +const advancedPanelCoords = computed(() => { + if (!mapHandler.canOpenAdvancedPanel()) { + return null; + } + return selectedVertex.value?.length ? toLonLat(selectedVertex.value) : null; +}); + const showSecondaryControls = computed(() => { return !props.disabled && (mapHandler.canUndoChange() || mapHandler.canDeleteFeatureOrVertex()); }); @@ -126,6 +137,16 @@ const undoLastChange = () => { mapHandler.undoLastChange(); emitSavedFeature(); }; + +const updateFeatureCoords = (newCoords: Coordinate[] | Coordinate[][]) => { + mapHandler.updateFeatureCoordinates(newCoords); + emitSavedFeature(); +}; + +const saveAdvancedPanelCoords = (newCoords: Coordinate) => { + mapHandler.updateVertexCoords(newCoords); + emitSavedFeature(); +}; diff --git a/packages/web-forms/src/components/common/map/MapUpdateCoordsDialog.vue b/packages/web-forms/src/components/common/map/MapUpdateCoordsDialog.vue new file mode 100644 index 000000000..60cacbbf1 --- /dev/null +++ b/packages/web-forms/src/components/common/map/MapUpdateCoordsDialog.vue @@ -0,0 +1,306 @@ + + +