From 42bbb8368e41fa4d25df270d44b12fbdd7908cf8 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Fri, 11 Oct 2024 14:57:17 +0200 Subject: [PATCH 1/9] feat(draw): enable editing circles on map keep circles as circle geometries in ol and convert them only to polygons for URL encoding --- src/composables/draw/draw-utils.ts | 40 ++++++++++++++----- .../draw/drawn-features.composable.ts | 2 - src/services/draw/drawn-feature.ts | 19 +++++++++ .../state-persistor-features.mapper.ts | 25 +++++++++--- .../state-persistor/utils/FeatureHash.ts | 2 - 5 files changed, 70 insertions(+), 18 deletions(-) diff --git a/src/composables/draw/draw-utils.ts b/src/composables/draw/draw-utils.ts index eb13c6d6..2d5aee52 100644 --- a/src/composables/draw/draw-utils.ts +++ b/src/composables/draw/draw-utils.ts @@ -1,6 +1,8 @@ -import { Circle, Geometry } from 'ol/geom' -import { fromCircle } from 'ol/geom/Polygon' -import { Feature } from 'ol' +import { Circle } from 'ol/geom' +import Polygon, { fromCircle } from 'ol/geom/Polygon' +import { getDistance } from 'ol/sphere' +import { toLonLat } from 'ol/proj' +import { DrawnFeature } from '@/services/draw/drawn-feature' // TODO 3D // import { transform } from 'ol/proj' @@ -9,14 +11,34 @@ import { Feature } from 'ol' // TODO 3D // const ARROW_MODEL_URL = import.meta.env.VITE_ARROW_MODEL_URL -function convertCircleToPolygon( - feature: Feature, - featureType: String -) { +function convertCircleFeatureToPolygon(feature: DrawnFeature): DrawnFeature { const geom = feature.getGeometry() - if (featureType == 'drawnCircle' && geom?.getType() == 'Circle') { + if (feature.featureType == 'drawnCircle' && geom?.getType() == 'Circle') { feature.setGeometry(fromCircle(geom as Circle, 64)) } + return feature } -export { convertCircleToPolygon } +function convertPolygonFeatureToCircle(feature: DrawnFeature): DrawnFeature { + const polygon = feature.getGeometry() as Polygon + if ( + feature.featureType === 'drawnCircle' && + polygon?.getType() === 'Polygon' + ) { + const extent = polygon.getExtent() + const centroid = [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2] + const coordinates = polygon.getCoordinates()[0] + let maxDistance = 0 + coordinates.forEach(coord => { + const distance = getDistance(toLonLat(centroid), toLonLat(coord)) + if (distance > maxDistance) { + maxDistance = distance + } + }) + const circle = new Circle(centroid, maxDistance) + feature.setGeometry(circle as Circle) + } + return feature +} + +export { convertCircleFeatureToPolygon, convertPolygonFeatureToCircle } diff --git a/src/composables/draw/drawn-features.composable.ts b/src/composables/draw/drawn-features.composable.ts index c13a3d35..1fc1e6dd 100644 --- a/src/composables/draw/drawn-features.composable.ts +++ b/src/composables/draw/drawn-features.composable.ts @@ -4,7 +4,6 @@ import { Feature } from 'ol' import { Point, Circle, Geometry, LineString } from 'ol/geom' import Polygon from 'ol/geom/Polygon' import { useDrawStore } from '@/stores/draw.store' -import { convertCircleToPolygon } from '@/composables/draw/draw-utils' import { useAppStore } from '@/stores/app.store' import { screenSizeIsAtLeast } from '@/services/common/device.utils' @@ -64,7 +63,6 @@ export default function useDrawnFeatures() { featureType, featureStyle, }) - convertCircleToPolygon(drawnFeature, featureType) addDrawnFeature(drawnFeature) diff --git a/src/services/draw/drawn-feature.ts b/src/services/draw/drawn-feature.ts index c17331ba..622f1288 100644 --- a/src/services/draw/drawn-feature.ts +++ b/src/services/draw/drawn-feature.ts @@ -31,6 +31,25 @@ export class DrawnFeature extends Feature { featureStyle: DrawnFeatureStyle map = useMap().getOlMap() + constructor(drawnFeature?: DrawnFeature) { + if (drawnFeature) { + super(drawnFeature.getGeometry()) + this.label = drawnFeature.label + this.featureType = drawnFeature.featureType + this.map_id = drawnFeature.map_id + this.description = drawnFeature.description + this.display_order = drawnFeature.display_order + this.editable = drawnFeature.editable + this.selected = drawnFeature.selected + this.featureStyle = drawnFeature.featureStyle + this.id = drawnFeature.id + this.saving = drawnFeature.saving + this.setProperties(drawnFeature.getProperties()) + } else { + super() + } + } + fit() { const size = this.map.getSize() const extent = this.getGeometry()?.getExtent() diff --git a/src/services/state-persistor/state-persistor-features.mapper.ts b/src/services/state-persistor/state-persistor-features.mapper.ts index 2614e2b3..398bb688 100644 --- a/src/services/state-persistor/state-persistor-features.mapper.ts +++ b/src/services/state-persistor/state-persistor-features.mapper.ts @@ -1,11 +1,26 @@ +import { + convertCircleFeatureToPolygon, + convertPolygonFeatureToCircle, +} from '@/composables/draw/draw-utils' import featureHash from './utils/FeatureHash' import { DrawnFeature } from '@/services/draw/drawn-feature' +/** + * Note that the mapper converts circles to polygons and back to circles. + * This allows one single ol modify interaction to edit all types of geometries + * and keeps them encoded as polygons in the URL (as in v3). + * It is important that persist creates seperate feature instances to get rid of the reference to the ol feature + */ class StorageFeaturesMapper { featuresToUrl(features: DrawnFeature[] | null): string { if (!features) return '' - const featuresToEncode = features.filter(feature => !feature.map_id) - featuresToEncode.forEach(f => f.set('name', f.label)) + const featuresToEncode = features + .filter(feature => !feature.map_id) + .map(feature => { + const newFeature = new DrawnFeature(feature) //create new instance to avoid converting ol feature to polygon + // newFeature.set('name', newFeature.label) //is this still needed? + return convertCircleFeatureToPolygon(newFeature) + }) return featuresToEncode.length > 0 ? featureHash.writeFeatures(featuresToEncode) : '' @@ -13,9 +28,9 @@ class StorageFeaturesMapper { urlToFeatures(url: string | null): DrawnFeature[] { const features = url ? featureHash.readFeatures(url) : [] - const drawnFeatures = features.map((f, i) => - featureHash.decodeShortProperties(f, i) - ) + const drawnFeatures = features + .map((feature, i) => featureHash.decodeShortProperties(feature, i)) + .map(feature => convertPolygonFeatureToCircle(feature)) return drawnFeatures } } diff --git a/src/services/state-persistor/utils/FeatureHash.ts b/src/services/state-persistor/utils/FeatureHash.ts index fc0655f7..03685827 100644 --- a/src/services/state-persistor/utils/FeatureHash.ts +++ b/src/services/state-persistor/utils/FeatureHash.ts @@ -36,7 +36,6 @@ import Fill from 'ol/style/Fill' import Style from 'ol/style/Style' import { DrawnFeature } from '@/services/draw/drawn-feature' import { DrawnFeatureStyle } from '@/stores/draw.store.model' -import { convertCircleToPolygon } from '@/composables/draw/draw-utils' const GeometryTypeValues = { LineString: 'LineString', @@ -306,7 +305,6 @@ class FeatureHash extends TextFeature { featureStyle: drawnFeatureStyle, } ) - convertCircleToPolygon(drawnFeature, featureType) return drawnFeature // TODO check defaults: From 6f536c94288279ba949c6e95e4652df653d993f7 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Fri, 11 Oct 2024 16:32:09 +0200 Subject: [PATCH 2/9] feat(draw): calculate circle radius and enable editing circle radius in panel --- src/components/draw/feature-measurements.vue | 46 +++++++++++++++----- src/composables/draw/edit.composable.ts | 13 ++++++ src/services/common/measurement.utils.ts | 18 ++++---- 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/components/draw/feature-measurements.vue b/src/components/draw/feature-measurements.vue index d01b7999..31bf4c37 100644 --- a/src/components/draw/feature-measurements.vue +++ b/src/components/draw/feature-measurements.vue @@ -8,13 +8,14 @@ import { getFormattedArea, getFormattedLength, } from '@/services/common/measurement.utils' -import { Geometry, Point, Polygon } from 'ol/geom' +import { Circle, Geometry, Point, Polygon } from 'ol/geom' import { Projection } from 'ol/proj' import useMap from '@/composables/map/map.composable' import { getDebouncedElevation, getElevation, } from './feature-measurements-helper' +import useEdit from '@/composables/draw/edit.composable' defineProps<{ isEditingFeature?: boolean @@ -27,21 +28,26 @@ const feature = ref(inject('feature')) const featureType = ref(feature.value?.featureType || '') const featureGeometry = ref(feature.value?.getGeometry()) +//TODO: update for circle const featLength = computed(() => featureGeometry.value && - ['drawnLine', 'drawnCircle', 'drawnPolygon'].includes(featureType.value) + ['drawnLine', 'drawnPolygon'].includes(featureType.value) ? getFormattedLength(featureGeometry.value as Geometry, mapProjection) : undefined ) +//TODO: update for circle const featArea = computed(() => - featureGeometry.value && - ['drawnPolygon', 'drawnCircle'].includes(featureType.value) + featureGeometry.value && ['drawnPolygon'].includes(featureType.value) ? getFormattedArea(featureGeometry.value as Polygon) : undefined ) -// TODO: implement once circle is kept as a circle geometry, -// also adapt length and area calculation for circle then -const featRadius = feature.value?.id + ' [TODO featRayon]' // TODO +const featRadius = computed(() => + featureGeometry.value && featureType.value === 'drawnCircle' + ? (featureGeometry.value as Circle).getRadius().toFixed(2) + : // ? getCircleRadius(featureGeometry.value as Circle, mapProjection) + undefined +) +const inputRadius = ref(featRadius.value || '') const featElevation = ref() @@ -58,8 +64,18 @@ watchEffect(async () => { } }) -function onClickValidateRadius() { - alert('TODO: validate /save radius') +watchEffect(() => { + inputRadius.value = (featureGeometry.value as Circle).getRadius().toFixed(2) + // inputRadius.value = getCircleRadius( + // featureGeometry.value as Circle, + // mapProjection + // ) +}) + +function onClickValidateRadius(radius: string) { + if (feature.value) { + useEdit().setRadius(feature.value as DrawnFeature, Number(radius)) + } } @@ -85,8 +101,16 @@ function onClickValidateRadius() { {{ featRadius }}
- -
diff --git a/src/composables/draw/edit.composable.ts b/src/composables/draw/edit.composable.ts index 260ea568..01840748 100644 --- a/src/composables/draw/edit.composable.ts +++ b/src/composables/draw/edit.composable.ts @@ -11,6 +11,7 @@ import { useDrawStore } from '@/stores/draw.store' import useMap from '../map/map.composable' import { EditStateActive } from '@/stores/draw.store.model' import { DEFAULT_DRAW_ZINDEX, FEATURE_LAYER_TYPE } from './draw.composable' +import { Circle } from 'ol/geom' export default function useEdit() { const { editStateActive, editingFeatureId, drawnFeatures } = storeToRefs( @@ -81,4 +82,16 @@ export default function useEdit() { updateDrawnFeature(feature as DrawnFeature) }) } + + function setRadius(feature: DrawnFeature, radius: number) { + const geometry = feature.getGeometry() + if (geometry?.getType() === 'Circle') { + ;(geometry as Circle).setRadius(radius) + updateDrawnFeature(feature) + } + } + + return { + setRadius, + } } diff --git a/src/services/common/measurement.utils.ts b/src/services/common/measurement.utils.ts index 82dac332..3b291b7a 100644 --- a/src/services/common/measurement.utils.ts +++ b/src/services/common/measurement.utils.ts @@ -1,5 +1,5 @@ import { Coordinate } from 'ol/coordinate' -import { LineString, Polygon, Point, Geometry } from 'ol/geom' +import { LineString, Polygon, Point, Geometry, Circle } from 'ol/geom' import { Projection, transform } from 'ol/proj' import { getDistance as haversineDistance, getArea } from 'ol/sphere' @@ -49,14 +49,13 @@ const getFormattedArea = function (polygon: Polygon): string { // return format(area, 'm²', 'square', precision) // } -// TODO: migrate and use once circle is kept as a circle geometry -// const getCircleRadius = function (circle: Circle, proj: Projection): string { -// const coord = circle.getLastCoordinate() -// const center = circle.getCenter() -// return center !== null && coord !== null -// ? getFormattedLength(new LineString([center, coord]), proj) -// : '' -// } +const getCircleRadius = function (circle: Circle, proj: Projection): string { + const coord = circle.getLastCoordinate() + const center = circle.getCenter() + return center !== null && coord !== null + ? getFormattedLength(new LineString([center, coord]), proj) + : '' +} const getFormattedAzimutRadius = function ( line: LineString, @@ -96,4 +95,5 @@ export { getFormattedArea, getFormattedAzimutRadius, getFormattedPoint, + getCircleRadius, } From 8173d6c52191dd1886f239dba5f5d58fd972e600 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Fri, 11 Oct 2024 17:32:08 +0200 Subject: [PATCH 3/9] feat(draw): calculate circle length and area --- src/components/draw/feature-measurements.vue | 42 ++++++++++++++------ src/services/common/measurement.utils.ts | 42 +++++++++++++++----- 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/components/draw/feature-measurements.vue b/src/components/draw/feature-measurements.vue index 31bf4c37..5a5bd66b 100644 --- a/src/components/draw/feature-measurements.vue +++ b/src/components/draw/feature-measurements.vue @@ -6,6 +6,8 @@ import { DrawnFeature } from '@/services/draw/drawn-feature' import FeatureMeasurementsProfile from './feature-measurements-profile.vue' import { getFormattedArea, + getFormattedCircleArea, + getFormattedCircleLength, getFormattedLength, } from '@/services/common/measurement.utils' import { Circle, Geometry, Point, Polygon } from 'ol/geom' @@ -28,19 +30,33 @@ const feature = ref(inject('feature')) const featureType = ref(feature.value?.featureType || '') const featureGeometry = ref(feature.value?.getGeometry()) -//TODO: update for circle -const featLength = computed(() => - featureGeometry.value && - ['drawnLine', 'drawnPolygon'].includes(featureType.value) - ? getFormattedLength(featureGeometry.value as Geometry, mapProjection) - : undefined -) -//TODO: update for circle -const featArea = computed(() => - featureGeometry.value && ['drawnPolygon'].includes(featureType.value) - ? getFormattedArea(featureGeometry.value as Polygon) - : undefined -) +const featLength = computed(() => { + if (featureGeometry.value) { + if (['drawnLine', 'drawnPolygon'].includes(featureType.value)) { + return getFormattedLength( + featureGeometry.value as Geometry, + mapProjection + ) + } else if (featureType.value === 'drawnCircle') { + return getFormattedCircleLength(featureGeometry.value as Circle) + } else { + return undefined + } + } + return undefined +}) +const featArea = computed(() => { + if (featureGeometry.value) { + if (featureType.value === 'drawnPolygon') { + return getFormattedArea(featureGeometry.value as Polygon) + } else if (featureType.value === 'drawnCircle') { + return getFormattedCircleArea(featureGeometry.value as Circle) + } else { + return undefined + } + } + return undefined +}) const featRadius = computed(() => featureGeometry.value && featureType.value === 'drawnCircle' ? (featureGeometry.value as Circle).getRadius().toFixed(2) diff --git a/src/services/common/measurement.utils.ts b/src/services/common/measurement.utils.ts index 3b291b7a..58767e8a 100644 --- a/src/services/common/measurement.utils.ts +++ b/src/services/common/measurement.utils.ts @@ -22,6 +22,33 @@ const getFormattedLength = function ( const c2 = transform(coordinates[i + 1], projection, 'EPSG:4326') length += haversineDistance(c1, c2) } + return formatLength(length, precision || 3) +} + +const getFormattedArea = function ( + polygon: Polygon, + precision?: number +): string { + const area = Math.abs(getArea(polygon)) + return formatArea(area, precision || 3) +} + +//TODO: handle projection ? +const getFormattedCircleLength = function ( + circle: Circle, + precision?: number +): string { + const radius = circle.getRadius() + const length = 2 * Math.PI * radius + return formatLength(length, precision || 3) +} + +const getFormattedCircleArea = function (circle: Circle, precision?: number) { + const area = Math.PI * Math.pow(circle.getRadius(), 2) + return formatArea(area, precision || 3) +} + +function formatLength(length: number, precision: number): string { let output if (length > 1000) { output = @@ -32,23 +59,16 @@ const getFormattedLength = function ( return output } -const getFormattedArea = function (polygon: Polygon): string { - const area = Math.abs(getArea(polygon)) +function formatArea(area: number, precision: number) { let output = '' if (area > 1000000) { - output = parseFloat((area / 1000000).toPrecision(3)) + ' ' + 'km²' + output = parseFloat((area / 1000000).toPrecision(precision)) + ' ' + 'km²' } else { - output = parseFloat(area.toPrecision(3)) + ' ' + 'm²' + output = parseFloat(area.toPrecision(precision)) + ' ' + 'm²' } return output } -// TODO: migrate and use once circle is kept as a circle geometry -// const getFormattedCircleArea = function (circle, precision, format) { -// const area = Math.PI * Math.pow(circle.getRadius(), 2) -// return format(area, 'm²', 'square', precision) -// } - const getCircleRadius = function (circle: Circle, proj: Projection): string { const coord = circle.getLastCoordinate() const center = circle.getCenter() @@ -95,5 +115,7 @@ export { getFormattedArea, getFormattedAzimutRadius, getFormattedPoint, + getFormattedCircleLength, + getFormattedCircleArea, getCircleRadius, } From e3156f02125ffb2ebb0e9b63be06d139b8836d65 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 16 Oct 2024 12:05:38 +0200 Subject: [PATCH 4/9] refactor(measurements): move length and area formatting to directives --- cypress/e2e/draw/draw-feat-line.cy.ts | 4 +- cypress/e2e/draw/draw-feat-polygon.cy.ts | 8 +- src/bundle/lib.ts | 6 +- src/components/draw/feature-measurements.vue | 41 ++++----- src/composables/draw/draw-tooltip.ts | 13 ++- src/directives/format-area.directive.ts | 33 +++++++ ...irective.ts => format-length.directive.ts} | 17 ++-- src/main.ts | 6 +- src/services/common/measurement.utils.ts | 88 ++++++------------- .../utils/FeatureStyleHelper.ts | 15 ++-- 10 files changed, 121 insertions(+), 110 deletions(-) create mode 100644 src/directives/format-area.directive.ts rename src/directives/{format-distance.directive.ts => format-length.directive.ts} (60%) diff --git a/cypress/e2e/draw/draw-feat-line.cy.ts b/cypress/e2e/draw/draw-feat-line.cy.ts index 14434710..ada0f825 100644 --- a/cypress/e2e/draw/draw-feat-line.cy.ts +++ b/cypress/e2e/draw/draw-feat-line.cy.ts @@ -28,9 +28,9 @@ describe('Draw "Line"', () => { }) it('updates length measurement when editing geometry', () => { - cy.get('*[data-cy="featItemLength"]').should('contain.text', '55.4 km') + cy.get('*[data-cy="featItemLength"]').should('contain.text', '55.36 km') cy.dragVertexOnMap(200, 200, 300, 300) - cy.get('*[data-cy="featItemLength"]').should('contain.text', '111 km') + cy.get('*[data-cy="featItemLength"]').should('contain.text', '111.14 km') }) it('displays the possible actions for the feature', () => { diff --git a/cypress/e2e/draw/draw-feat-polygon.cy.ts b/cypress/e2e/draw/draw-feat-polygon.cy.ts index ee8ae345..6d387d35 100644 --- a/cypress/e2e/draw/draw-feat-polygon.cy.ts +++ b/cypress/e2e/draw/draw-feat-polygon.cy.ts @@ -28,11 +28,11 @@ describe('Draw "Polygon"', () => { }) it('updates length and area measurements when editing geometry', () => { - cy.get('*[data-cy="featItemLength"]').should('contain.text', '134 km') - cy.get('*[data-cy="featItemArea"]').should('contain.text', '766 km²') + cy.get('*[data-cy="featItemLength"]').should('contain.text', '133.81 km') + cy.get('*[data-cy="featItemArea"]').should('contain.text', '766.33 km²') cy.dragVertexOnMap(200, 200, 300, 300) - cy.get('*[data-cy="featItemLength"]').should('contain.text', '238 km') - cy.get('*[data-cy="featItemArea"]').should('contain.text', '1530 km²') + cy.get('*[data-cy="featItemLength"]').should('contain.text', '238.47 km') + cy.get('*[data-cy="featItemArea"]').should('contain.text', '1532.65 km²') }) it('displays the possible actions for the feature', () => { diff --git a/src/bundle/lib.ts b/src/bundle/lib.ts index 15912186..f2b525a6 100644 --- a/src/bundle/lib.ts +++ b/src/bundle/lib.ts @@ -49,7 +49,8 @@ import { clearLayersCache } from '@/stores/layers.cache' import i18next, { InitOptions } from 'i18next' import backend from 'i18next-http-backend' import I18NextVue from 'i18next-vue' -import formatDistanceDirective from '@/directives/format-distance.directive' +import formatLengthDirective from '@/directives/format-length.directive' +import formatAreaDirective from '@/directives/format-area.directive' import App from '../App.vue' @@ -79,7 +80,8 @@ export default function useLuxLib(options: LuxLibOptions) { app.use(createPinia()) app.use(I18NextVue, { i18next }) app.use(VueDOMPurifyHTML) - app.use(formatDistanceDirective) + app.use(formatLengthDirective) + app.use(formatAreaDirective) const createElementInstance = (component = {}, app = null) => { return defineCustomElement( diff --git a/src/components/draw/feature-measurements.vue b/src/components/draw/feature-measurements.vue index 5a5bd66b..458d24df 100644 --- a/src/components/draw/feature-measurements.vue +++ b/src/components/draw/feature-measurements.vue @@ -5,10 +5,10 @@ import { useTranslation } from 'i18next-vue' import { DrawnFeature } from '@/services/draw/drawn-feature' import FeatureMeasurementsProfile from './feature-measurements-profile.vue' import { - getFormattedArea, - getFormattedCircleArea, - getFormattedCircleLength, - getFormattedLength, + getArea, + getCircleArea, + getCircleLength, + getLength, } from '@/services/common/measurement.utils' import { Circle, Geometry, Point, Polygon } from 'ol/geom' import { Projection } from 'ol/proj' @@ -33,12 +33,9 @@ const featureGeometry = ref(feature.value?.getGeometry()) const featLength = computed(() => { if (featureGeometry.value) { if (['drawnLine', 'drawnPolygon'].includes(featureType.value)) { - return getFormattedLength( - featureGeometry.value as Geometry, - mapProjection - ) + return getLength(featureGeometry.value as Geometry, mapProjection) } else if (featureType.value === 'drawnCircle') { - return getFormattedCircleLength(featureGeometry.value as Circle) + return getCircleLength(featureGeometry.value as Circle) } else { return undefined } @@ -48,9 +45,9 @@ const featLength = computed(() => { const featArea = computed(() => { if (featureGeometry.value) { if (featureType.value === 'drawnPolygon') { - return getFormattedArea(featureGeometry.value as Polygon) + return getArea(featureGeometry.value as Polygon) } else if (featureType.value === 'drawnCircle') { - return getFormattedCircleArea(featureGeometry.value as Circle) + return getCircleArea(featureGeometry.value as Circle) } else { return undefined } @@ -59,11 +56,11 @@ const featArea = computed(() => { }) const featRadius = computed(() => featureGeometry.value && featureType.value === 'drawnCircle' - ? (featureGeometry.value as Circle).getRadius().toFixed(2) + ? (featureGeometry.value as Circle).getRadius() : // ? getCircleRadius(featureGeometry.value as Circle, mapProjection) undefined ) -const inputRadius = ref(featRadius.value || '') +const inputRadius = ref(featRadius.value?.toString() || '') const featElevation = ref() @@ -81,7 +78,10 @@ watchEffect(async () => { }) watchEffect(() => { - inputRadius.value = (featureGeometry.value as Circle).getRadius().toFixed(2) + inputRadius.value = + featureType.value === 'drawnCircle' + ? (featureGeometry.value as Circle).getRadius().toFixed(2) + : '' // inputRadius.value = getCircleRadius( // featureGeometry.value as Circle, // mapProjection @@ -99,12 +99,12 @@ function onClickValidateRadius(radius: string) {
- {{ t('Length:') }} {{ featLength }} + {{ t('Length:') }}
- {{ t('Area:') }} {{ featArea }} + {{ t('Area:') }}
@@ -113,8 +113,8 @@ function onClickValidateRadius(radius: string) { v-if="featureType === 'drawnCircle'" class="flex items-center" > - {{ t('Rayon:') }} - {{ featRadius }} + {{ t('Rayon:') }} +
{{ t('Elevation') }}: - +
diff --git a/src/composables/draw/draw-tooltip.ts b/src/composables/draw/draw-tooltip.ts index 335316a5..efc10fc4 100644 --- a/src/composables/draw/draw-tooltip.ts +++ b/src/composables/draw/draw-tooltip.ts @@ -5,10 +5,9 @@ import { unByKey } from 'ol/Observable' import { Circle, Geometry, LineString, Polygon } from 'ol/geom' import OlMap from 'ol/Map' import { DrawEvent } from 'ol/interaction/Draw' -import { - getFormattedLength, - getFormattedArea, -} from '@/services/common/measurement.utils' +import { getLength, getArea } from '@/services/common/measurement.utils' +import { formatLength } from '@/directives/format-length.directive' +import { formatArea } from '@/directives/format-area.directive' class DrawTooltip { private measureTooltipElement: HTMLElement | null = null @@ -69,7 +68,7 @@ class DrawTooltip { const geom = geometry as LineString coord = geom.getLastCoordinate() if (coord !== null) { - output = getFormattedLength(geom, proj) + output = formatLength(getLength(geom, proj)) } } else if (geometry.getType() === 'Polygon') { const geom = geometry as Polygon @@ -78,14 +77,14 @@ class DrawTooltip { coord = geom.getInteriorPoint().getCoordinates() } if (coord !== null) { - output = getFormattedArea(geom) + output = formatArea(getArea(geom)) } } else if (geometry.getType() === 'Circle') { const geom = geometry as Circle coord = geom.getLastCoordinate() const center = geom.getCenter() if (center !== null && coord !== null) { - output = getFormattedLength(new LineString([center, coord]), proj) + output = formatLength(getLength(new LineString([center, coord]), proj)) } } if (this.measureTooltipElement) { diff --git a/src/directives/format-area.directive.ts b/src/directives/format-area.directive.ts new file mode 100644 index 00000000..3da7a1eb --- /dev/null +++ b/src/directives/format-area.directive.ts @@ -0,0 +1,33 @@ +import i18next from 'i18next' +import { App } from 'vue' + +export default { + install(app: App) { + app.directive('format-area', { + beforeMount(el: HTMLElement, binding: { value: number }) { + format(el, binding.value) + }, + updated(el: HTMLElement, binding: { value: number }) { + format(el, binding.value) + }, + }) + }, +} + +function format(el: HTMLElement, value: number): void { + let formattedText: string = '' + formattedText = formatArea(value) + el.textContent = formattedText +} + +export function formatArea(value: number): string { + if (value === null) { + return i18next.t('N/A', { ns: 'client' }) + } else if (value < 1000000) { + return `${value.toFixed(2)} m²` + } else if (value >= 1000000) { + return `${(value / 1000000).toFixed(2)} km²` + } else { + return '' + } +} diff --git a/src/directives/format-distance.directive.ts b/src/directives/format-length.directive.ts similarity index 60% rename from src/directives/format-distance.directive.ts rename to src/directives/format-length.directive.ts index 111899fd..0c7a2c68 100644 --- a/src/directives/format-distance.directive.ts +++ b/src/directives/format-length.directive.ts @@ -3,7 +3,7 @@ import { App } from 'vue' export default { install(app: App) { - app.directive('format-distance', { + app.directive('format-length', { beforeMount(el: HTMLElement, binding: { value: number }) { format(el, binding.value) }, @@ -16,12 +16,19 @@ export default { function format(el: HTMLElement, value: number): void { let formattedText: string = '' + formattedText = formatLength(value) + el.textContent = formattedText +} + +export function formatLength(value: number): string { + //null covers API errors or unaivalble data (eg. elevation) if (value === null) { - formattedText = i18next.t('N/A', { ns: 'client' }) + return i18next.t('N/A', { ns: 'client' }) } else if (value < 1000) { - formattedText = `${value.toFixed(2)} m` + return `${value.toFixed(2)} m` } else if (value >= 1000) { - formattedText = `${(value / 1000).toFixed(2)} km` + return `${(value / 1000).toFixed(2)} km` + } else { + return '' } - el.textContent = formattedText } diff --git a/src/main.ts b/src/main.ts index 89169226..22364a5f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,8 @@ import { createPinia } from 'pinia' import { initProjections } from '@/services/projection.utils' import { useThemeStore } from './stores/config.store' import { themesApiFixture } from './__fixtures__/themes.api.fixture' -import formatDistanceDirective from '@/directives/format-distance.directive' +import formatLengthDirective from '@/directives/format-length.directive' +import formatAreaDirective from '@/directives/format-area.directive' import App from './App.vue' @@ -38,7 +39,8 @@ const app = createApp(App) app.use(createPinia()) app.use(I18NextVue, { i18next }) app.use(VueDOMPurifyHTML) -app.use(formatDistanceDirective) +app.use(formatLengthDirective) +app.use(formatAreaDirective) app.mount('#app') diff --git a/src/services/common/measurement.utils.ts b/src/services/common/measurement.utils.ts index 58767e8a..f01dd472 100644 --- a/src/services/common/measurement.utils.ts +++ b/src/services/common/measurement.utils.ts @@ -1,92 +1,61 @@ +import { formatLength } from '@/directives/format-length.directive' import { Coordinate } from 'ol/coordinate' import { LineString, Polygon, Point, Geometry, Circle } from 'ol/geom' import { Projection, transform } from 'ol/proj' -import { getDistance as haversineDistance, getArea } from 'ol/sphere' +import { + getDistance as haversineDistance, + getArea as getOlArea, +} from 'ol/sphere' -const getFormattedLength = function ( - geom: Geometry, - projection: Projection, - precision?: number -): string { +const getLength = function (geom: Geometry, projection: Projection): number { let length = 0 - let coordinates + let coordinates: Coordinate[] = [] if (geom.getType() === 'Polygon') { coordinates = (geom as Polygon).getCoordinates()[0] as Coordinate[] } else if (geom.getType() === 'LineString') { coordinates = (geom as LineString).getCoordinates() as Coordinate[] - } else { - return '' } for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { const c1 = transform(coordinates[i], projection, 'EPSG:4326') const c2 = transform(coordinates[i + 1], projection, 'EPSG:4326') length += haversineDistance(c1, c2) } - return formatLength(length, precision || 3) + return length } -const getFormattedArea = function ( - polygon: Polygon, - precision?: number -): string { - const area = Math.abs(getArea(polygon)) - return formatArea(area, precision || 3) +const getArea = function (polygon: Polygon): number { + return Math.abs(getOlArea(polygon)) } //TODO: handle projection ? -const getFormattedCircleLength = function ( - circle: Circle, - precision?: number -): string { +const getCircleLength = function (circle: Circle): number { const radius = circle.getRadius() - const length = 2 * Math.PI * radius - return formatLength(length, precision || 3) -} - -const getFormattedCircleArea = function (circle: Circle, precision?: number) { - const area = Math.PI * Math.pow(circle.getRadius(), 2) - return formatArea(area, precision || 3) -} - -function formatLength(length: number, precision: number): string { - let output - if (length > 1000) { - output = - parseFloat((length / 1000).toPrecision(precision || 3)) + ' ' + 'km' - } else { - output = parseFloat(length.toPrecision(precision || 3)) + ' ' + 'm' - } - return output + return 2 * Math.PI * radius } -function formatArea(area: number, precision: number) { - let output = '' - if (area > 1000000) { - output = parseFloat((area / 1000000).toPrecision(precision)) + ' ' + 'km²' - } else { - output = parseFloat(area.toPrecision(precision)) + ' ' + 'm²' - } - return output +const getCircleArea = function (circle: Circle): number { + return Math.PI * Math.pow(circle.getRadius(), 2) } -const getCircleRadius = function (circle: Circle, proj: Projection): string { +const getCircleRadius = function ( + circle: Circle, + proj: Projection +): number | undefined { const coord = circle.getLastCoordinate() const center = circle.getCenter() return center !== null && coord !== null - ? getFormattedLength(new LineString([center, coord]), proj) - : '' + ? getLength(new LineString([center, coord]), proj) + : undefined } +//The following functions still contain formatting logic as the returned values are not used in the DOM const getFormattedAzimutRadius = function ( line: LineString, projection: Projection, - decimals: number, - precision: number + decimals: number ) { let output = getFormattedAzimut(line, decimals) - - output += `, ${getFormattedLength(line, projection, precision)}` - + output += `, ${formatLength(getLength(line, projection))}` return output } @@ -110,12 +79,13 @@ const getFormattedPoint = function (point: Point, decimals: number) { .map(c => c.toPrecision(decimals)) .join(' ') } + export { - getFormattedLength, - getFormattedArea, + getLength, + getArea, + getCircleLength, + getCircleArea, + getCircleRadius, getFormattedAzimutRadius, getFormattedPoint, - getFormattedCircleLength, - getFormattedCircleArea, - getCircleRadius, } diff --git a/src/services/state-persistor/utils/FeatureStyleHelper.ts b/src/services/state-persistor/utils/FeatureStyleHelper.ts index 32958234..0c8cd3fa 100644 --- a/src/services/state-persistor/utils/FeatureStyleHelper.ts +++ b/src/services/state-persistor/utils/FeatureStyleHelper.ts @@ -22,11 +22,13 @@ import olStyleStyle from 'ol/style/Style' import olStyleText from 'ol/style/Text' import * as olProj from 'ol/proj' import { - getFormattedLength, - getFormattedArea, + getLength, + getArea, getFormattedAzimutRadius, getFormattedPoint, } from '@/services/common/measurement.utils' +import { formatLength } from '@/directives/format-length.directive' +import { formatArea } from '@/directives/format-area.directive' const styleGeometryType = { CIRCLE: 'Circle', @@ -250,7 +252,7 @@ class FeatureStyleHelper { if (showMeasure && azimut !== undefined) { // Radius style: const line = this.getRadiusLine(feature, azimut) - const length = getFormattedLength(line, this.projection_!) //, this.precision_, this.unitPrefixFormat_); + const length = formatLength(getLength(line, this.projection_!)) //, this.precision_, this.unitPrefixFormat_); styles.push( new olStyleStyle({ @@ -629,14 +631,13 @@ class FeatureStyleHelper { measure = getFormattedAzimutRadius( line, this.projection_!, - this.decimals_, - this.precision_ || 3 + this.decimals_ ) } else { - measure = getFormattedArea(geometry) //, this.projection_, this.precision_, this.unitPrefixFormat_); + measure = formatArea(getArea(geometry)) //, this.projection_, this.precision_, this.unitPrefixFormat_); } } else if (geometry instanceof olGeomLineString) { - measure = getFormattedLength(geometry, this.projection_!) //, this.precision_, this.unitPrefixFormat_); + measure = formatLength(getLength(geometry, this.projection_!)) //, this.precision_, this.unitPrefixFormat_); } else if (geometry instanceof olGeomPoint) { if (this.pointFilterFn_ === null) { measure = getFormattedPoint(geometry, this.decimals_) From ceb4c3e3d9ce1190205c40eff83eca94981dd101 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 16 Oct 2024 17:20:40 +0200 Subject: [PATCH 5/9] fix(measurements): take into account projection for circle measurements one issue that remains is that the circle radius is not an exact value and decreases on multiple submits or app reloads: - submitting same values multiple times also has an issue on prod - reloading the app on prod may work because enocoded polygon geometries are not converted back to circles --- src/components/draw/feature-measurements.vue | 18 +++++----- src/composables/draw/draw-utils.ts | 7 +++- src/composables/draw/edit.composable.ts | 3 +- src/services/common/measurement.utils.ts | 35 ++++++++++++++------ 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/components/draw/feature-measurements.vue b/src/components/draw/feature-measurements.vue index 458d24df..2c935d0f 100644 --- a/src/components/draw/feature-measurements.vue +++ b/src/components/draw/feature-measurements.vue @@ -8,6 +8,7 @@ import { getArea, getCircleArea, getCircleLength, + getCircleRadius, getLength, } from '@/services/common/measurement.utils' import { Circle, Geometry, Point, Polygon } from 'ol/geom' @@ -35,7 +36,7 @@ const featLength = computed(() => { if (['drawnLine', 'drawnPolygon'].includes(featureType.value)) { return getLength(featureGeometry.value as Geometry, mapProjection) } else if (featureType.value === 'drawnCircle') { - return getCircleLength(featureGeometry.value as Circle) + return getCircleLength(featureGeometry.value as Circle, mapProjection) } else { return undefined } @@ -47,7 +48,7 @@ const featArea = computed(() => { if (featureType.value === 'drawnPolygon') { return getArea(featureGeometry.value as Polygon) } else if (featureType.value === 'drawnCircle') { - return getCircleArea(featureGeometry.value as Circle) + return getCircleArea(featureGeometry.value as Circle, mapProjection) } else { return undefined } @@ -56,9 +57,8 @@ const featArea = computed(() => { }) const featRadius = computed(() => featureGeometry.value && featureType.value === 'drawnCircle' - ? (featureGeometry.value as Circle).getRadius() - : // ? getCircleRadius(featureGeometry.value as Circle, mapProjection) - undefined + ? getCircleRadius(featureGeometry.value as Circle, mapProjection) + : undefined ) const inputRadius = ref(featRadius.value?.toString() || '') @@ -80,12 +80,10 @@ watchEffect(async () => { watchEffect(() => { inputRadius.value = featureType.value === 'drawnCircle' - ? (featureGeometry.value as Circle).getRadius().toFixed(2) + ? getCircleRadius(featureGeometry.value as Circle, mapProjection).toFixed( + 2 + ) : '' - // inputRadius.value = getCircleRadius( - // featureGeometry.value as Circle, - // mapProjection - // ) }) function onClickValidateRadius(radius: string) { diff --git a/src/composables/draw/draw-utils.ts b/src/composables/draw/draw-utils.ts index 2d5aee52..ada860b3 100644 --- a/src/composables/draw/draw-utils.ts +++ b/src/composables/draw/draw-utils.ts @@ -3,6 +3,9 @@ import Polygon, { fromCircle } from 'ol/geom/Polygon' import { getDistance } from 'ol/sphere' import { toLonLat } from 'ol/proj' import { DrawnFeature } from '@/services/draw/drawn-feature' +import { setCircleRadius } from '@/services/common/measurement.utils' +import useMap from '../map/map.composable' +import { Map } from 'ol' // TODO 3D // import { transform } from 'ol/proj' @@ -20,6 +23,7 @@ function convertCircleFeatureToPolygon(feature: DrawnFeature): DrawnFeature { } function convertPolygonFeatureToCircle(feature: DrawnFeature): DrawnFeature { + const map: Map = useMap().getOlMap() const polygon = feature.getGeometry() as Polygon if ( feature.featureType === 'drawnCircle' && @@ -35,7 +39,8 @@ function convertPolygonFeatureToCircle(feature: DrawnFeature): DrawnFeature { maxDistance = distance } }) - const circle = new Circle(centroid, maxDistance) + const circle = new Circle(centroid) + setCircleRadius(circle, maxDistance, map) feature.setGeometry(circle as Circle) } return feature diff --git a/src/composables/draw/edit.composable.ts b/src/composables/draw/edit.composable.ts index 01840748..798a1488 100644 --- a/src/composables/draw/edit.composable.ts +++ b/src/composables/draw/edit.composable.ts @@ -12,6 +12,7 @@ import useMap from '../map/map.composable' import { EditStateActive } from '@/stores/draw.store.model' import { DEFAULT_DRAW_ZINDEX, FEATURE_LAYER_TYPE } from './draw.composable' import { Circle } from 'ol/geom' +import { setCircleRadius } from '@/services/common/measurement.utils' export default function useEdit() { const { editStateActive, editingFeatureId, drawnFeatures } = storeToRefs( @@ -86,7 +87,7 @@ export default function useEdit() { function setRadius(feature: DrawnFeature, radius: number) { const geometry = feature.getGeometry() if (geometry?.getType() === 'Circle') { - ;(geometry as Circle).setRadius(radius) + setCircleRadius(geometry as Circle, radius, map) updateDrawnFeature(feature) } } diff --git a/src/services/common/measurement.utils.ts b/src/services/common/measurement.utils.ts index f01dd472..3675c1e2 100644 --- a/src/services/common/measurement.utils.ts +++ b/src/services/common/measurement.utils.ts @@ -1,11 +1,17 @@ import { formatLength } from '@/directives/format-length.directive' import { Coordinate } from 'ol/coordinate' import { LineString, Polygon, Point, Geometry, Circle } from 'ol/geom' -import { Projection, transform } from 'ol/proj' +import { + getPointResolution, + METERS_PER_UNIT, + Projection, + transform, +} from 'ol/proj' import { getDistance as haversineDistance, getArea as getOlArea, } from 'ol/sphere' +import { Map } from 'ol' const getLength = function (geom: Geometry, projection: Projection): number { let length = 0 @@ -27,25 +33,31 @@ const getArea = function (polygon: Polygon): number { return Math.abs(getOlArea(polygon)) } -//TODO: handle projection ? -const getCircleLength = function (circle: Circle): number { - const radius = circle.getRadius() +const getCircleLength = function (circle: Circle, proj: Projection): number { + const radius = getCircleRadius(circle, proj) return 2 * Math.PI * radius } -const getCircleArea = function (circle: Circle): number { - return Math.PI * Math.pow(circle.getRadius(), 2) +const getCircleArea = function (circle: Circle, proj: Projection): number { + return Math.PI * Math.pow(getCircleRadius(circle, proj), 2) } -const getCircleRadius = function ( - circle: Circle, - proj: Projection -): number | undefined { +const getCircleRadius = function (circle: Circle, proj: Projection): number { const coord = circle.getLastCoordinate() const center = circle.getCenter() return center !== null && coord !== null ? getLength(new LineString([center, coord]), proj) - : undefined + : 0 +} + +const setCircleRadius = function (circle: Circle, radius: number, map: Map) { + const center = circle.getCenter() + const projection = map.getView().getProjection() + const resolution = map.getView().getResolution() || 0 + const pointResolution = getPointResolution(projection, resolution, center) + const resolutionFactor = resolution / pointResolution + radius = (radius / METERS_PER_UNIT.m) * resolutionFactor + circle.setRadius(radius) } //The following functions still contain formatting logic as the returned values are not used in the DOM @@ -86,6 +98,7 @@ export { getCircleLength, getCircleArea, getCircleRadius, + setCircleRadius, getFormattedAzimutRadius, getFormattedPoint, } From 82fe53d85dbe504d4d9383153abcdbf6cb87638d Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Wed, 16 Oct 2024 17:29:32 +0200 Subject: [PATCH 6/9] fix(modify): remove geometry from map on delete with edition mode active --- src/stores/draw.store.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stores/draw.store.ts b/src/stores/draw.store.ts index f4bd7936..8e80e6ad 100644 --- a/src/stores/draw.store.ts +++ b/src/stores/draw.store.ts @@ -76,6 +76,7 @@ export const useDrawStore = defineStore('draw', () => { drawnFeatures.value = drawnFeatures.value.filter( feature => feature.id !== featureId ) + editingFeatureId.value = undefined } return { From 8daedde62e103a6c714582c1ddc0585daf90e468 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Thu, 17 Oct 2024 12:10:49 +0200 Subject: [PATCH 7/9] fix(export): convert circles to polygons for export --- src/services/export-feature/export-feature.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/services/export-feature/export-feature.ts b/src/services/export-feature/export-feature.ts index f95b57ef..da32f617 100644 --- a/src/services/export-feature/export-feature.ts +++ b/src/services/export-feature/export-feature.ts @@ -4,6 +4,8 @@ import { Geometry, GeometryCollection, MultiLineString } from 'ol/geom' import { PROJECTION_WGS84 } from '@/composables/map/map.composable' import { downloadFile, sanitizeFilename } from '@/services/utils' +import { convertCircleFeatureToPolygon } from '@/composables/draw/draw-utils' +import { DrawnFeature } from '../draw/drawn-feature' export abstract class ExportFeature { encodeOptions: { dataProjection: string; featureProjection: Projection } @@ -61,6 +63,11 @@ export abstract class ExportFeature { ) break } + case 'Circle': { + const newFeature = new DrawnFeature(feature as DrawnFeature) + explodedFeatures.push(convertCircleFeatureToPolygon(newFeature)) + break + } default: explodedFeatures.push(feature) break From 9c0bc186fc764e708b3ea6f2dea4d35ee81e37ce Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Thu, 17 Oct 2024 12:11:41 +0200 Subject: [PATCH 8/9] test(e2e): add tests for circle measurements --- cypress/e2e/draw/draw-feat-circle.cy.ts | 18 ++++++++++++++++++ src/components/draw/feature-measurements.vue | 1 + 2 files changed, 19 insertions(+) diff --git a/cypress/e2e/draw/draw-feat-circle.cy.ts b/cypress/e2e/draw/draw-feat-circle.cy.ts index 62b25694..d7c0f27f 100644 --- a/cypress/e2e/draw/draw-feat-circle.cy.ts +++ b/cypress/e2e/draw/draw-feat-circle.cy.ts @@ -26,6 +26,24 @@ describe('Draw "Circle"', () => { testFeatItemMeasurements() }) + it('updates length, area and radius measurements when editing geometry on map', () => { + cy.get('*[data-cy="featItemLength"]').should('contain.text', '346.59 km') + cy.get('*[data-cy="featItemArea"]').should('contain.text', '9559.11 km²') + cy.get('*[data-cy="featItemInputRadius"]').should( + 'have.value', + '55161.21' + ) + cy.dragVertexOnMap(200, 200, 300, 300) + cy.get('*[data-cy="featItemLength"]').should('contain.text', '693.17 km') + cy.get('*[data-cy="featItemArea"]').should('contain.text', '38235.40 km²') + }) + + it('updates length and area measurements when editing radius in panel', () => { + cy.get('*[data-cy="featItemInputRadius"]').type('{selectall}1000{enter}') + cy.get('*[data-cy="featItemLength"]').should('contain.text', '6.28 km') + cy.get('*[data-cy="featItemArea"]').should('contain.text', '3.13 km²') + }) + it('displays the possible actions for the feature', () => { testFeatItem() }) diff --git a/src/components/draw/feature-measurements.vue b/src/components/draw/feature-measurements.vue index 2c935d0f..0785bf45 100644 --- a/src/components/draw/feature-measurements.vue +++ b/src/components/draw/feature-measurements.vue @@ -116,6 +116,7 @@ function onClickValidateRadius(radius: string) {
Date: Fri, 18 Oct 2024 10:37:42 +0200 Subject: [PATCH 9/9] refactor(review): address comments --- src/components/draw/feature-measurements.vue | 21 ++++++------ src/composables/draw/draw-tooltip.ts | 3 +- src/composables/draw/draw-utils.ts | 15 ++++++++- src/directives/format-area.directive.ts | 14 +------- src/directives/format-length.directive.ts | 15 +-------- src/services/common/formatting.utils.ts | 33 +++++++++++++++++++ src/services/common/measurement.utils.ts | 7 ++-- .../utils/FeatureStyleHelper.ts | 7 ++-- 8 files changed, 67 insertions(+), 48 deletions(-) diff --git a/src/components/draw/feature-measurements.vue b/src/components/draw/feature-measurements.vue index 0785bf45..9c8c9a0f 100644 --- a/src/components/draw/feature-measurements.vue +++ b/src/components/draw/feature-measurements.vue @@ -37,8 +37,6 @@ const featLength = computed(() => { return getLength(featureGeometry.value as Geometry, mapProjection) } else if (featureType.value === 'drawnCircle') { return getCircleLength(featureGeometry.value as Circle, mapProjection) - } else { - return undefined } } return undefined @@ -49,8 +47,6 @@ const featArea = computed(() => { return getArea(featureGeometry.value as Polygon) } else if (featureType.value === 'drawnCircle') { return getCircleArea(featureGeometry.value as Circle, mapProjection) - } else { - return undefined } } return undefined @@ -60,7 +56,7 @@ const featRadius = computed(() => ? getCircleRadius(featureGeometry.value as Circle, mapProjection) : undefined ) -const inputRadius = ref(featRadius.value?.toString() || '') +const inputRadius = ref(featRadius.value || 0) const featElevation = ref() @@ -80,13 +76,16 @@ watchEffect(async () => { watchEffect(() => { inputRadius.value = featureType.value === 'drawnCircle' - ? getCircleRadius(featureGeometry.value as Circle, mapProjection).toFixed( - 2 + ? parseFloat( + getCircleRadius( + featureGeometry.value as Circle, + mapProjection + ).toFixed(2) ) - : '' + : 0 }) -function onClickValidateRadius(radius: string) { +function onClickValidateRadius(radius: number) { if (feature.value) { useEdit().setRadius(feature.value as DrawnFeature, Number(radius)) } @@ -117,8 +116,8 @@ function onClickValidateRadius(radius: string) {
diff --git a/src/composables/draw/draw-tooltip.ts b/src/composables/draw/draw-tooltip.ts index efc10fc4..ed54e75c 100644 --- a/src/composables/draw/draw-tooltip.ts +++ b/src/composables/draw/draw-tooltip.ts @@ -6,8 +6,7 @@ import { Circle, Geometry, LineString, Polygon } from 'ol/geom' import OlMap from 'ol/Map' import { DrawEvent } from 'ol/interaction/Draw' import { getLength, getArea } from '@/services/common/measurement.utils' -import { formatLength } from '@/directives/format-length.directive' -import { formatArea } from '@/directives/format-area.directive' +import { formatLength, formatArea } from '@/services/common/formatting.utils' class DrawTooltip { private measureTooltipElement: HTMLElement | null = null diff --git a/src/composables/draw/draw-utils.ts b/src/composables/draw/draw-utils.ts index ada860b3..37d7c73d 100644 --- a/src/composables/draw/draw-utils.ts +++ b/src/composables/draw/draw-utils.ts @@ -14,14 +14,27 @@ import { Map } from 'ol' // TODO 3D // const ARROW_MODEL_URL = import.meta.env.VITE_ARROW_MODEL_URL +/** + * Note that feature.featureType and geom?.getType() values mostly correspond to each other. + * One exception are 'drawnCircle' featureTypes that are managed as 'Polygon' geometries within the URL and during export. + * (Another exception are 'drawnLabel' featureTypes that are managed as 'Point' geometries throughout the application.) + * @param feature Feature with a circle geometry + * @returns The same feature with a polygon geometry + */ function convertCircleFeatureToPolygon(feature: DrawnFeature): DrawnFeature { const geom = feature.getGeometry() - if (feature.featureType == 'drawnCircle' && geom?.getType() == 'Circle') { + if (feature.featureType === 'drawnCircle' && geom?.getType() === 'Circle') { feature.setGeometry(fromCircle(geom as Circle, 64)) } return feature } +/** + * + * @param feature Feature with a polygon geometry + * @returns The same feature with a circle geometry + */ + function convertPolygonFeatureToCircle(feature: DrawnFeature): DrawnFeature { const map: Map = useMap().getOlMap() const polygon = feature.getGeometry() as Polygon diff --git a/src/directives/format-area.directive.ts b/src/directives/format-area.directive.ts index 3da7a1eb..8d13b3ae 100644 --- a/src/directives/format-area.directive.ts +++ b/src/directives/format-area.directive.ts @@ -1,4 +1,4 @@ -import i18next from 'i18next' +import { formatArea } from '@/services/common/formatting.utils' import { App } from 'vue' export default { @@ -19,15 +19,3 @@ function format(el: HTMLElement, value: number): void { formattedText = formatArea(value) el.textContent = formattedText } - -export function formatArea(value: number): string { - if (value === null) { - return i18next.t('N/A', { ns: 'client' }) - } else if (value < 1000000) { - return `${value.toFixed(2)} m²` - } else if (value >= 1000000) { - return `${(value / 1000000).toFixed(2)} km²` - } else { - return '' - } -} diff --git a/src/directives/format-length.directive.ts b/src/directives/format-length.directive.ts index 0c7a2c68..5b4c263a 100644 --- a/src/directives/format-length.directive.ts +++ b/src/directives/format-length.directive.ts @@ -1,4 +1,4 @@ -import i18next from 'i18next' +import { formatLength } from '@/services/common/formatting.utils' import { App } from 'vue' export default { @@ -19,16 +19,3 @@ function format(el: HTMLElement, value: number): void { formattedText = formatLength(value) el.textContent = formattedText } - -export function formatLength(value: number): string { - //null covers API errors or unaivalble data (eg. elevation) - if (value === null) { - return i18next.t('N/A', { ns: 'client' }) - } else if (value < 1000) { - return `${value.toFixed(2)} m` - } else if (value >= 1000) { - return `${(value / 1000).toFixed(2)} km` - } else { - return '' - } -} diff --git a/src/services/common/formatting.utils.ts b/src/services/common/formatting.utils.ts index c163c50b..cdab1021 100644 --- a/src/services/common/formatting.utils.ts +++ b/src/services/common/formatting.utils.ts @@ -1,4 +1,37 @@ +import i18next from 'i18next' + +/** + * Note: Formatting utils can be used via directives in HTML templates. + * v-format-length="value" + * v-format-area="value" + */ + export function formatDate(dateString: string, language: string = 'fr-FR') { const date = new Date(dateString) return new Intl.DateTimeFormat(language).format(date) } + +export function formatLength(value: number): string { + //null covers API errors or unavailable data (eg. elevation) + if (value === null) { + return i18next.t('N/A', { ns: 'client' }) + } else if (value < 1000) { + return `${value.toFixed(2)} m` + } else if (value >= 1000) { + return `${(value / 1000).toFixed(2)} km` + } else { + return '' + } +} + +export function formatArea(value: number): string { + if (value === null) { + return i18next.t('N/A', { ns: 'client' }) + } else if (value < 1000000) { + return `${value.toFixed(2)} m²` + } else if (value >= 1000000) { + return `${(value / 1000000).toFixed(2)} km²` + } else { + return '' + } +} diff --git a/src/services/common/measurement.utils.ts b/src/services/common/measurement.utils.ts index 3675c1e2..fb0b4057 100644 --- a/src/services/common/measurement.utils.ts +++ b/src/services/common/measurement.utils.ts @@ -1,4 +1,4 @@ -import { formatLength } from '@/directives/format-length.directive' +import { formatLength } from './formatting.utils' import { Coordinate } from 'ol/coordinate' import { LineString, Polygon, Point, Geometry, Circle } from 'ol/geom' import { @@ -12,6 +12,7 @@ import { getArea as getOlArea, } from 'ol/sphere' import { Map } from 'ol' +import { PROJECTION_WGS84 } from '@/composables/map/map.composable' const getLength = function (geom: Geometry, projection: Projection): number { let length = 0 @@ -22,8 +23,8 @@ const getLength = function (geom: Geometry, projection: Projection): number { coordinates = (geom as LineString).getCoordinates() as Coordinate[] } for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { - const c1 = transform(coordinates[i], projection, 'EPSG:4326') - const c2 = transform(coordinates[i + 1], projection, 'EPSG:4326') + const c1 = transform(coordinates[i], projection, PROJECTION_WGS84) + const c2 = transform(coordinates[i + 1], projection, PROJECTION_WGS84) length += haversineDistance(c1, c2) } return length diff --git a/src/services/state-persistor/utils/FeatureStyleHelper.ts b/src/services/state-persistor/utils/FeatureStyleHelper.ts index 0c8cd3fa..b630232b 100644 --- a/src/services/state-persistor/utils/FeatureStyleHelper.ts +++ b/src/services/state-persistor/utils/FeatureStyleHelper.ts @@ -27,8 +27,7 @@ import { getFormattedAzimutRadius, getFormattedPoint, } from '@/services/common/measurement.utils' -import { formatLength } from '@/directives/format-length.directive' -import { formatArea } from '@/directives/format-area.directive' +import { formatLength, formatArea } from '@/services/common/formatting.utils' const styleGeometryType = { CIRCLE: 'Circle', @@ -634,10 +633,10 @@ class FeatureStyleHelper { this.decimals_ ) } else { - measure = formatArea(getArea(geometry)) //, this.projection_, this.precision_, this.unitPrefixFormat_); + measure = formatArea(getArea(geometry)) } } else if (geometry instanceof olGeomLineString) { - measure = formatLength(getLength(geometry, this.projection_!)) //, this.precision_, this.unitPrefixFormat_); + measure = formatLength(getLength(geometry, this.projection_!)) } else if (geometry instanceof olGeomPoint) { if (this.pointFilterFn_ === null) { measure = getFormattedPoint(geometry, this.decimals_)