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/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 ca843355..7db6daf0 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 d01b7999..9c8c9a0f 100644 --- a/src/components/draw/feature-measurements.vue +++ b/src/components/draw/feature-measurements.vue @@ -5,16 +5,20 @@ import { useTranslation } from 'i18next-vue' import { DrawnFeature } from '@/services/draw/drawn-feature' import FeatureMeasurementsProfile from './feature-measurements-profile.vue' import { - getFormattedArea, - getFormattedLength, + getArea, + getCircleArea, + getCircleLength, + getCircleRadius, + getLength, } 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 +31,32 @@ const feature = ref(inject('feature')) const featureType = ref(feature.value?.featureType || '') const featureGeometry = ref(feature.value?.getGeometry()) -const featLength = computed(() => - featureGeometry.value && - ['drawnLine', 'drawnCircle', 'drawnPolygon'].includes(featureType.value) - ? getFormattedLength(featureGeometry.value as Geometry, mapProjection) - : undefined -) -const featArea = computed(() => - featureGeometry.value && - ['drawnPolygon', 'drawnCircle'].includes(featureType.value) - ? getFormattedArea(featureGeometry.value as Polygon) +const featLength = computed(() => { + if (featureGeometry.value) { + if (['drawnLine', 'drawnPolygon'].includes(featureType.value)) { + return getLength(featureGeometry.value as Geometry, mapProjection) + } else if (featureType.value === 'drawnCircle') { + return getCircleLength(featureGeometry.value as Circle, mapProjection) + } + } + return undefined +}) +const featArea = computed(() => { + if (featureGeometry.value) { + if (featureType.value === 'drawnPolygon') { + return getArea(featureGeometry.value as Polygon) + } else if (featureType.value === 'drawnCircle') { + return getCircleArea(featureGeometry.value as Circle, mapProjection) + } + } + return undefined +}) +const featRadius = computed(() => + featureGeometry.value && featureType.value === 'drawnCircle' + ? getCircleRadius(featureGeometry.value as Circle, mapProjection) : 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 inputRadius = ref(featRadius.value || 0) const featElevation = ref() @@ -58,8 +73,22 @@ watchEffect(async () => { } }) -function onClickValidateRadius() { - alert('TODO: validate /save radius') +watchEffect(() => { + inputRadius.value = + featureType.value === 'drawnCircle' + ? parseFloat( + getCircleRadius( + featureGeometry.value as Circle, + mapProjection + ).toFixed(2) + ) + : 0 +}) + +function onClickValidateRadius(radius: number) { + if (feature.value) { + useEdit().setRadius(feature.value as DrawnFeature, Number(radius)) + } } @@ -67,12 +96,12 @@ function onClickValidateRadius() {
- {{ t('Length:') }} {{ featLength }} + {{ t('Length:') }}
- {{ t('Area:') }} {{ featArea }} + {{ t('Area:') }}
@@ -81,12 +110,21 @@ function onClickValidateRadius() { v-if="featureType === 'drawnCircle'" class="flex items-center" > - {{ t('Rayon:') }} - {{ featRadius }} + {{ t('Rayon:') }} +
- -
@@ -95,10 +133,7 @@ function onClickValidateRadius() {
{{ t('Elevation') }}: - +
diff --git a/src/composables/draw/draw-tooltip.ts b/src/composables/draw/draw-tooltip.ts index 335316a5..ed54e75c 100644 --- a/src/composables/draw/draw-tooltip.ts +++ b/src/composables/draw/draw-tooltip.ts @@ -5,10 +5,8 @@ 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, formatArea } from '@/services/common/formatting.utils' class DrawTooltip { private measureTooltipElement: HTMLElement | null = null @@ -69,7 +67,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 +76,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/composables/draw/draw-utils.ts b/src/composables/draw/draw-utils.ts index eb13c6d6..37d7c73d 100644 --- a/src/composables/draw/draw-utils.ts +++ b/src/composables/draw/draw-utils.ts @@ -1,6 +1,11 @@ -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' +import { setCircleRadius } from '@/services/common/measurement.utils' +import useMap from '../map/map.composable' +import { Map } from 'ol' // TODO 3D // import { transform } from 'ol/proj' @@ -9,14 +14,49 @@ import { Feature } from 'ol' // TODO 3D // const ARROW_MODEL_URL = import.meta.env.VITE_ARROW_MODEL_URL -function convertCircleToPolygon( - feature: Feature, - featureType: String -) { +/** + * 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 (featureType == 'drawnCircle' && geom?.getType() == 'Circle') { + if (feature.featureType === 'drawnCircle' && geom?.getType() === 'Circle') { feature.setGeometry(fromCircle(geom as Circle, 64)) } + return feature } -export { convertCircleToPolygon } +/** + * + * @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 + 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) + setCircleRadius(circle, maxDistance, map) + 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/composables/draw/edit.composable.ts b/src/composables/draw/edit.composable.ts index 260ea568..798a1488 100644 --- a/src/composables/draw/edit.composable.ts +++ b/src/composables/draw/edit.composable.ts @@ -11,6 +11,8 @@ 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' +import { setCircleRadius } from '@/services/common/measurement.utils' export default function useEdit() { const { editStateActive, editingFeatureId, drawnFeatures } = storeToRefs( @@ -81,4 +83,16 @@ export default function useEdit() { updateDrawnFeature(feature as DrawnFeature) }) } + + function setRadius(feature: DrawnFeature, radius: number) { + const geometry = feature.getGeometry() + if (geometry?.getType() === 'Circle') { + setCircleRadius(geometry as Circle, radius, map) + updateDrawnFeature(feature) + } + } + + return { + setRadius, + } } diff --git a/src/directives/format-distance.directive.ts b/src/directives/format-area.directive.ts similarity index 57% rename from src/directives/format-distance.directive.ts rename to src/directives/format-area.directive.ts index 111899fd..8d13b3ae 100644 --- a/src/directives/format-distance.directive.ts +++ b/src/directives/format-area.directive.ts @@ -1,9 +1,9 @@ -import i18next from 'i18next' +import { formatArea } from '@/services/common/formatting.utils' import { App } from 'vue' export default { install(app: App) { - app.directive('format-distance', { + app.directive('format-area', { beforeMount(el: HTMLElement, binding: { value: number }) { format(el, binding.value) }, @@ -16,12 +16,6 @@ export default { function format(el: HTMLElement, value: number): void { let formattedText: string = '' - if (value === null) { - formattedText = i18next.t('N/A', { ns: 'client' }) - } else if (value < 1000) { - formattedText = `${value.toFixed(2)} m` - } else if (value >= 1000) { - formattedText = `${(value / 1000).toFixed(2)} km` - } + formattedText = formatArea(value) el.textContent = formattedText } diff --git a/src/directives/format-length.directive.ts b/src/directives/format-length.directive.ts new file mode 100644 index 00000000..5b4c263a --- /dev/null +++ b/src/directives/format-length.directive.ts @@ -0,0 +1,21 @@ +import { formatLength } from '@/services/common/formatting.utils' +import { App } from 'vue' + +export default { + install(app: App) { + app.directive('format-length', { + 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 = formatLength(value) + 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/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 82dac332..fb0b4057 100644 --- a/src/services/common/measurement.utils.ts +++ b/src/services/common/measurement.utils.ts @@ -1,73 +1,74 @@ +import { formatLength } from './formatting.utils' import { Coordinate } from 'ol/coordinate' -import { LineString, Polygon, Point, Geometry } from 'ol/geom' -import { Projection, transform } from 'ol/proj' -import { getDistance as haversineDistance, getArea } from 'ol/sphere' +import { LineString, Polygon, Point, Geometry, Circle } from 'ol/geom' +import { + getPointResolution, + METERS_PER_UNIT, + Projection, + transform, +} from 'ol/proj' +import { + getDistance as haversineDistance, + getArea as getOlArea, +} from 'ol/sphere' +import { Map } from 'ol' +import { PROJECTION_WGS84 } from '@/composables/map/map.composable' -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') + const c1 = transform(coordinates[i], projection, PROJECTION_WGS84) + const c2 = transform(coordinates[i + 1], projection, PROJECTION_WGS84) length += haversineDistance(c1, c2) } - let output - if (length > 1000) { - output = - parseFloat((length / 1000).toPrecision(precision || 3)) + ' ' + 'km' - } else { - output = parseFloat(length.toPrecision(precision || 3)) + ' ' + 'm' - } - return output + return length } -const getFormattedArea = function (polygon: Polygon): string { - const area = Math.abs(getArea(polygon)) - let output = '' - if (area > 1000000) { - output = parseFloat((area / 1000000).toPrecision(3)) + ' ' + 'km²' - } else { - output = parseFloat(area.toPrecision(3)) + ' ' + 'm²' - } - return output +const getArea = function (polygon: Polygon): number { + return Math.abs(getOlArea(polygon)) +} + +const getCircleLength = function (circle: Circle, proj: Projection): number { + const radius = getCircleRadius(circle, proj) + return 2 * Math.PI * radius } -// 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 getCircleArea = function (circle: Circle, proj: Projection): number { + return Math.PI * Math.pow(getCircleRadius(circle, proj), 2) +} + +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) + : 0 +} -// 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 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 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 } @@ -91,9 +92,14 @@ const getFormattedPoint = function (point: Point, decimals: number) { .map(c => c.toPrecision(decimals)) .join(' ') } + export { - getFormattedLength, - getFormattedArea, + getLength, + getArea, + getCircleLength, + getCircleArea, + getCircleRadius, + setCircleRadius, getFormattedAzimutRadius, getFormattedPoint, } 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/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 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: diff --git a/src/services/state-persistor/utils/FeatureStyleHelper.ts b/src/services/state-persistor/utils/FeatureStyleHelper.ts index 32958234..b630232b 100644 --- a/src/services/state-persistor/utils/FeatureStyleHelper.ts +++ b/src/services/state-persistor/utils/FeatureStyleHelper.ts @@ -22,11 +22,12 @@ 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, formatArea } from '@/services/common/formatting.utils' const styleGeometryType = { CIRCLE: 'Circle', @@ -250,7 +251,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 +630,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)) } } else if (geometry instanceof olGeomLineString) { - measure = getFormattedLength(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_) 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 {