Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/young-nails-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@getodk/web-forms': minor
---

Adds support for editing the coordinates of a vertex or point
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions packages/web-forms/src/components/common/IconSVG.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
mdiChevronDown,
mdiChevronUp,
mdiClose,
mdiCogOutline,
mdiContentSave,
mdiCrosshairsGps,
mdiDotsVertical,
Expand Down Expand Up @@ -48,6 +49,7 @@ const iconMap: Record<string, string> = {
mdiChevronDown,
mdiChevronUp,
mdiClose,
mdiCogOutline,
mdiContentSave,
mdiCrosshairsGps,
mdiDotsVertical,
Expand Down
8 changes: 3 additions & 5 deletions packages/web-forms/src/components/common/map/AsyncMap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@
* 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 } 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';

Expand Down
182 changes: 182 additions & 0 deletions packages/web-forms/src/components/common/map/MapAdvancedPanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<script setup lang="ts">
import IconSVG from '@/components/common/IconSVG.vue';
import { toGeoJsonCoordinateArray } from '@/components/common/map/geojson-parsers.ts';
import { fromLonLat } from 'ol/proj';
import { 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 accuracy = ref<number | undefined>();
const latitude = ref<number | undefined>();
const altitude = ref<number | undefined>();
const longitude = ref<number | undefined>();

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 (!props.coordinates?.length) {
return;
}

const [originalLong, originalLat] = props.coordinates;
const long = longitude.value ?? originalLong;
const lat = latitude.value ?? originalLat;
if (long === undefined || lat === undefined) {
return;
}

const newVertex = toGeoJsonCoordinateArray(
long,
lat,
altitude.value,
accuracy.value
) as Coordinate;
emit('save', fromLonLat(newVertex));
};
</script>

<template>
<transition name="panel">
<div v-if="isOpen" class="advanced-panel">
<div class="fields-container">
<div class="field-set">
<label for="longitude">Longitude</label>
<input id="longitude" v-model="longitude" type="number" @change="updateVertex">
</div>
<div class="field-set">
<label for="latitude">Latitude</label>
<input id="latitude" v-model="latitude" type="number" @change="updateVertex">
</div>
<div class="field-set">
<label for="altitude">Altitude</label>
<input id="altitude" v-model="altitude" type="number" @change="updateVertex">
</div>
<div class="field-set">
<label for="accuracy">Accuracy</label>
<input id="accuracy" v-model="accuracy" type="number" @change="updateVertex">
</div>
</div>

<a class="paste-location" @click="emit('open-paste-dialog')">
<IconSVG name="mdiFileOutline" size="sm" />
<strong>Paste location data</strong>
</a>
</div>
</transition>
</template>

<style scoped lang="scss">
.advanced-panel {
--odk-double-map-spacing: calc(var(--odk-map-controls-spacing) * 2);
}

.advanced-panel {
border-top: 1px solid var(--odk-border-color);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 3px;
}

.fields-container {
display: flex;
flex-wrap: wrap;
gap: var(--odk-double-map-spacing);
padding: var(--odk-double-map-spacing) var(--odk-map-controls-spacing);

.field-set {
display: flex;
border: 1px solid var(--odk-border-color);
border-radius: 6px;
overflow: hidden;
background-color: var(--odk-muted-background-color);
flex: 1 1 calc(50% - var(--odk-double-map-spacing));
height: 38px;
min-width: 250px;
}

label {
padding: var(--odk-map-controls-spacing);
background-color: var(--odk-light-background-color);
color: var(--odk-text-color);
font-weight: normal;
font-size: var(--odk-base-font-size);
display: flex;
align-items: center;
border-right: 1px solid var(--odk-border-color);
white-space: nowrap;
}

input {
padding: var(--odk-map-controls-spacing);
width: 100%;
background-color: var(--odk-base-background-color);
border: none;
font-size: var(--odk-base-font-size);
color: var(--odk-text-color);

&:focus-visible {
outline: none;
outline-offset: unset;
}

&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
appearance: none;
-webkit-appearance: none;
margin: 0;
}
}
}

.paste-location {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: var(--odk-double-map-spacing);
text-decoration: none;
cursor: pointer;
font-size: var(--odk-base-font-size);
color: var(--odk-text-color);
}

.panel-enter-active,
.panel-leave-active {
transition:
max-height 0.6s ease-in-out,
opacity 0.6s ease-in-out;
overflow: hidden;
}

.panel-enter-from,
.panel-leave-to {
max-height: 0;
opacity: 0;
}

.panel-enter-to,
.panel-leave-from {
max-height: 300px;
opacity: 1;
}
</style>
75 changes: 67 additions & 8 deletions packages/web-forms/src/components/common/map/MapBlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
*/
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 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 { Feature, FeatureCollection, Point as PointGeoJSON } 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';
Expand All @@ -32,7 +35,9 @@ const props = defineProps<MapBlockProps>();
const emit = defineEmits(['save']);
const mapElement = ref<HTMLElement | undefined>();
const isFullScreen = ref(false);
const isAdvancedPanelOpen = ref(false);
const confirmDeleteAction = ref(false);
const isUpdateCoordsDialogOpen = ref(false);
const selectedVertex = ref<Coordinate | undefined>();
const showErrorStyle = inject<ComputedRef<boolean>>(
QUESTION_HAS_ERROR,
Expand All @@ -43,10 +48,32 @@ const mapHandler = useMapBlock(
{ mode: props.mode, drawFeatureType: props.drawFeatureType },
{
onFeaturePlacement: () => emitSavedFeature(),
onVertexSelect: (vertex) => (selectedVertex.value = vertex),
onVertexSelect: (vertex) => {
selectedVertex.value = vertex;
if (!selectedVertex.value?.length) {
isAdvancedPanelOpen.value = false;
}
},
}
);

const advancedPanelCoords = computed<Coordinate | null>(() => {
if (!mapHandler.canOpenAdvacedPanel()) {
return null;
}

if (props.drawFeatureType && selectedVertex.value) {
return toLonLat(selectedVertex.value);
}

const geometry = props.savedFeatureValue?.geometry as PointGeoJSON | undefined;
if (geometry) {
return geometry.coordinates;
}

return null;
});

const showSecondaryControls = computed(() => {
return !props.disabled && (mapHandler.canUndoChange() || mapHandler.canDeleteFeatureOrVertex());
});
Expand Down Expand Up @@ -131,6 +158,21 @@ const undoLastChange = () => {
mapHandler.undoLastChange();
emitSavedFeature();
};

const updateFeatureCoords = (newCoords: Coordinate | Coordinate[] | Coordinate[][]) => {
mapHandler.updateFeatureCoordinates(newCoords);
emitSavedFeature();
};

const saveAdvancedPanelCoords = (newCoords: Coordinate) => {
if (props.drawFeatureType) {
mapHandler.updateVertexCoords(newCoords);
emitSavedFeature();
return;
}

updateFeatureCoords(newCoords);
};
</script>

<template>
Expand Down Expand Up @@ -172,17 +214,28 @@ const undoLastChange = () => {
</div>

<MapStatusBar
:can-enable-advanced-panel="!!advancedPanelCoords"
:can-open-advanced-panel="!disabled && mapHandler.canOpenAdvacedPanel()"
:can-remove="!disabled && mapHandler.canRemoveCurrentLocation()"
:can-save="!disabled && mapHandler.canSaveCurrentLocation()"
:can-view-details="mapHandler.canViewProperties()"
:draw-feature-type="drawFeatureType"
:is-capturing="mapHandler.currentState.value === STATES.CAPTURING"
:saved-feature-value="savedFeatureValue"
:selected-vertex="selectedVertex"
:is-capturing="mapHandler.currentState.value === STATES.CAPTURING"
class="map-status-bar-component"
:can-remove="!disabled && mapHandler.canRemoveCurrentLocation()"
:can-save="!disabled && mapHandler.canSaveCurrentLocation()"
:can-view-details="mapHandler.canViewProperties()"
@discard="discardSavedFeature"
@save="saveCurrentLocation"
@view-details="mapHandler.selectSavedFeature"
@open-advanced-panel="isAdvancedPanelOpen = !isAdvancedPanelOpen"
/>

<MapAdvancedPanel
v-if="!disabled && mapHandler.canOpenAdvacedPanel()"
:is-open="isAdvancedPanelOpen"
:coordinates="advancedPanelCoords"
@open-paste-dialog="isUpdateCoordsDialogOpen = true"
@save="saveAdvancedPanelCoords"
/>

<MapProperties
Expand All @@ -208,11 +261,17 @@ const undoLastChange = () => {
</div>
</div>

<MapConfirm
<MapConfirmDialog
v-model:visible="confirmDeleteAction"
:draw-feature-type="drawFeatureType"
@delete-feature="deleteFeature"
/>

<MapUpdateCoordsDialog
v-model:visible="isUpdateCoordsDialogOpen"
:draw-feature-type="drawFeatureType"
@save="updateFeatureCoords"
/>
</template>

<style scoped lang="scss">
Expand Down
Loading
Loading