From eddfdd3e01f2cadbca67af42e217fe0e8d03eba5 Mon Sep 17 00:00:00 2001 From: Pascal Barth Date: Thu, 20 Nov 2025 08:23:56 +0100 Subject: [PATCH 1/5] PB-2027: only request icon in LARGE size, and resize them with the scale icons were doubly scaled because the scale was applied on top of a smaller icon URL. Reverting back to what was the use-case before the TS migration, which is that icons are always requested in the same size to the backend, and are scaled on the map (could we get rid of the scaling on the backend?) --- packages/viewer/src/api/icon.api.ts | 30 +++++++----------- .../actions/updateCurrentDrawingFeature.ts | 31 +++++-------------- .../src/utils/__tests__/kmlUtils.spec.ts | 8 ++--- .../viewer/src/utils/featureStyleUtils.ts | 7 ++--- packages/viewer/src/utils/kmlUtils.ts | 8 ++--- 5 files changed, 29 insertions(+), 55 deletions(-) diff --git a/packages/viewer/src/api/icon.api.ts b/packages/viewer/src/api/icon.api.ts index 18df91957a..0a014b9f0f 100644 --- a/packages/viewer/src/api/icon.api.ts +++ b/packages/viewer/src/api/icon.api.ts @@ -3,12 +3,7 @@ import axios from 'axios' import { fromString } from 'ol/color' import { getViewerDedicatedServicesBaseUrl } from '@/config/baseUrl.config' -import { - type FeatureStyleColor, - type FeatureStyleSize, - LARGE, - RED, -} from '@/utils/featureStyleUtils' +import { type FeatureStyleColor, LARGE, RED } from '@/utils/featureStyleUtils' /** * Generate an icon URL from its template. If no iconScale is given, the default scale 1 will be @@ -17,19 +12,18 @@ import { * * @returns A full URL to this icon on the service-icons backend */ -export function generateIconURL( - icon: DrawingIcon, - iconColor: FeatureStyleColor = RED, - iconSize: FeatureStyleSize = LARGE -) { +export function generateIconURL(icon: DrawingIcon, iconColor: FeatureStyleColor = RED) { const rgb = fromString(iconColor.fill) - return icon.imageTemplateURL - .replace('{icon_set_name}', icon.iconSetName) - .replace('{icon_name}', icon.name) - .replace('{icon_scale}', iconSize.iconScale + 'x') - .replace('{r}', `${rgb[0]}`) - .replace('{g}', `${rgb[1]}`) - .replace('{b}', `${rgb[2]}`) + return ( + icon.imageTemplateURL + .replace('{icon_set_name}', icon.iconSetName) + .replace('{icon_name}', icon.name) + // we always use the LARGE icon scale and resize the icon with the property in KMLs + .replace('{icon_scale}', LARGE.iconScale + 'x') + .replace('{r}', `${rgb[0]}`) + .replace('{g}', `${rgb[1]}`) + .replace('{b}', `${rgb[2]}`) + ) } /** diff --git a/packages/viewer/src/store/modules/drawing/actions/updateCurrentDrawingFeature.ts b/packages/viewer/src/store/modules/drawing/actions/updateCurrentDrawingFeature.ts index bad7925d92..8cc71b62c2 100644 --- a/packages/viewer/src/store/modules/drawing/actions/updateCurrentDrawingFeature.ts +++ b/packages/viewer/src/store/modules/drawing/actions/updateCurrentDrawingFeature.ts @@ -8,13 +8,7 @@ import { generateIconURL } from '@/api/icon.api' import { DrawingSaveState } from '@/store/modules/drawing/types/DrawingSaveState.enum' import debounceSaveDrawing from '@/store/modules/drawing/utils/debounceSaveDrawing' import useProfileStore from '@/store/modules/profile' -import { - calculateTextOffset, - type FeatureStyleColor, - type FeatureStyleSize, - MEDIUM, - RED, -} from '@/utils/featureStyleUtils' +import { calculateTextOffset } from '@/utils/featureStyleUtils' export default function updateCurrentDrawingFeature( this: DrawingStore, @@ -24,21 +18,24 @@ export default function updateCurrentDrawingFeature( if (this.feature.current) { this.save.state = DrawingSaveState.UnsavedChanges - let needIconUrlRefresh = false - Object.assign(this.feature.current, valuesToUpdate) // keeping values as preferred, if present, so that the next time the user draws, the values are used if (valuesToUpdate.iconSize) { this.edit.preferred.size = valuesToUpdate.iconSize - needIconUrlRefresh = true } if (valuesToUpdate.textSize) { this.edit.preferred.size = valuesToUpdate.textSize } if (valuesToUpdate.fillColor) { this.edit.preferred.color = valuesToUpdate.fillColor - needIconUrlRefresh = true + // refreshing the icon color if present + if (this.feature.current.icon) { + this.feature.current.icon.imageURL = generateIconURL( + this.feature.current.icon, + valuesToUpdate.fillColor + ) + } } if (valuesToUpdate.textColor) { this.edit.preferred.color = valuesToUpdate.textColor @@ -62,18 +59,6 @@ export default function updateCurrentDrawingFeature( } } - if (this.feature.current.icon && needIconUrlRefresh) { - const newIconSize: FeatureStyleSize = - valuesToUpdate.iconSize ?? this.feature.current.iconSize ?? MEDIUM - const newIconColor: FeatureStyleColor = - valuesToUpdate.fillColor ?? this.feature.current.fillColor ?? RED - this.feature.current.icon.imageURL = generateIconURL( - this.feature.current.icon, - newIconColor, - newIconSize - ) - } - const profileStore = useProfileStore() // updating the profile feature if the currently drawn feature is the profile feature if (profileStore.feature?.id === this.feature.current.id) { diff --git a/packages/viewer/src/utils/__tests__/kmlUtils.spec.ts b/packages/viewer/src/utils/__tests__/kmlUtils.spec.ts index db07e1d2d4..dc38e84260 100644 --- a/packages/viewer/src/utils/__tests__/kmlUtils.spec.ts +++ b/packages/viewer/src/utils/__tests__/kmlUtils.spec.ts @@ -12,7 +12,7 @@ import type { EditableFeature } from '@/api/features.api' import { type DrawingIconSet, generateIconURL } from '@/api/icon.api' import { getServiceKmlBaseUrl } from '@/config/baseUrl.config' import { fakeIconSets } from '@/utils/__tests__/legacyKmlUtils.spec' -import { BLUE, EXTRA_LARGE } from '@/utils/featureStyleUtils' +import { BLUE } from '@/utils/featureStyleUtils' import { getIcon, getKmlExtent, @@ -350,7 +350,7 @@ describe('Test KML utils', () => { 'https://fake.image.url/api/icons/sets/default/icons/001-marker@1x-255,0,0.png' ) }) - it('get icon with standard arguments from the set with scale and color', () => { + it('get icon with standard arguments from the set with color', () => { const icon = getIcon( { set: 'default', @@ -363,8 +363,8 @@ describe('Test KML utils', () => { expect(icon).toBeDefined() expect(icon!.name).to.be.equal('001-marker') expect(icon!.iconSetName).to.be.equal('default') - expect(generateIconURL(icon!, BLUE, EXTRA_LARGE)).to.be.equal( - 'https://fake.image.url/api/icons/sets/default/icons/001-marker@1.25x-0,0,255.png' + expect(generateIconURL(icon!, BLUE)).to.be.equal( + 'https://fake.image.url/api/icons/sets/default/icons/001-marker@1x-0,0,255.png' ) }) it('get icon with standard arguments from the babs set', () => { diff --git a/packages/viewer/src/utils/featureStyleUtils.ts b/packages/viewer/src/utils/featureStyleUtils.ts index c2b10962c5..582b5450af 100644 --- a/packages/viewer/src/utils/featureStyleUtils.ts +++ b/packages/viewer/src/utils/featureStyleUtils.ts @@ -398,13 +398,10 @@ export function geoadminStyleFunction( let image: Icon | undefined if (editableFeature?.icon) { image = new Icon({ - src: generateIconURL( - editableFeature.icon, - editableFeature.fillColor, - editableFeature.iconSize - ), + src: generateIconURL(editableFeature.icon, editableFeature.fillColor), crossOrigin: 'Anonymous', anchor: editableFeature.icon.anchor, + scale: editableFeature.iconSize?.iconScale, }) } const styles = [ diff --git a/packages/viewer/src/utils/kmlUtils.ts b/packages/viewer/src/utils/kmlUtils.ts index 7c470ec0ed..3d13403400 100644 --- a/packages/viewer/src/utils/kmlUtils.ts +++ b/packages/viewer/src/utils/kmlUtils.ts @@ -1,11 +1,10 @@ import type { CoordinateSystem, FlatExtent } from '@swissgeo/coordinates' +import { WGS84 } from '@swissgeo/coordinates' import type { KMLLayer } from '@swissgeo/layers' +import { KMLStyle } from '@swissgeo/layers' import type { Geometry } from 'ol/geom' import type { Type as GeometryType } from 'ol/geom/Geometry' import type { Size } from 'ol/size' - -import { WGS84 } from '@swissgeo/coordinates' -import { KMLStyle } from '@swissgeo/layers' import log, { LogPreDefinedColor } from '@swissgeo/log' import { kml as kmlToGeoJSON } from '@tmcw/togeojson' import { booleanValid } from '@turf/turf' @@ -23,7 +22,6 @@ import IconStyle from 'ol/style/Icon' import Style from 'ol/style/Style' import type { EditableFeature } from '@/api/features.api' - import { EditableFeatureTypes, extractOlFeatureCoordinates } from '@/api/features.api' import { proxifyUrl } from '@/api/file-proxy.api' import { type DrawingIcon, type DrawingIconSet, generateIconURL } from '@/api/icon.api' @@ -554,7 +552,7 @@ export function getEditableFeatureFromKmlFeature( }) const image = style.getImage() if (image instanceof IconStyle) { - image.setSrc(generateIconURL(icon, fillColor, iconSize)) + image.setSrc(generateIconURL(icon, fillColor)) } } From 44d05809a15c1750abd4dd31ab4f6af9916c5ffa Mon Sep 17 00:00:00 2001 From: Pascal Barth Date: Thu, 20 Nov 2025 10:20:03 +0100 Subject: [PATCH 2/5] PB-2027: fix code so that marker/text test passes simplifying a bit the useFieldValidation composable, and trying to maintain reactivity of props given there, even if it is two components down (EmailInput calls TextInput which calls useFieldValidation) --- .../styling/DrawingStyleMediaLink.vue | 8 +- .../modules/drawing/actions/closeDrawing.ts | 1 - .../src/utils/components/EmailInput.vue | 174 +++++++++--------- .../viewer/src/utils/components/FileInput.vue | 26 +-- .../src/utils/components/TextAreaInput.vue | 159 ++++++++-------- .../viewer/src/utils/components/TextInput.vue | 99 +++++----- .../utils/composables/useFieldValidation.ts | 153 +++++++-------- packages/viewer/src/utils/kmlUtils.ts | 6 +- .../viewer/tests/cypress/support/drawing.ts | 10 +- .../tests/cypress/tests-e2e/drawing.cy.ts | 25 ++- 10 files changed, 319 insertions(+), 342 deletions(-) diff --git a/packages/viewer/src/modules/infobox/components/styling/DrawingStyleMediaLink.vue b/packages/viewer/src/modules/infobox/components/styling/DrawingStyleMediaLink.vue index 7d19edc4d5..2449c228e9 100644 --- a/packages/viewer/src/modules/infobox/components/styling/DrawingStyleMediaLink.vue +++ b/packages/viewer/src/modules/infobox/components/styling/DrawingStyleMediaLink.vue @@ -17,8 +17,8 @@ const emit = defineEmits<{ }>() const { t } = useI18n() -const generatedMediaLink = ref(undefined) -const linkDescription = ref(undefined) +const generatedMediaLink = ref() +const linkDescription = ref() const isFormValid = ref(false) const activateValidation = ref(false) @@ -117,14 +117,14 @@ function onUrlValidate(result: TextInputValidateResult): void { placeholder="paste_url" :validate="validateUrl" data-cy="drawing-style-media-url" - @keydown.enter="addLink()" + @keydown.enter="addLink" @validate="onUrlValidate" > diff --git a/packages/viewer/src/store/modules/drawing/actions/closeDrawing.ts b/packages/viewer/src/store/modules/drawing/actions/closeDrawing.ts index 96983a23f5..db5da37961 100644 --- a/packages/viewer/src/store/modules/drawing/actions/closeDrawing.ts +++ b/packages/viewer/src/store/modules/drawing/actions/closeDrawing.ts @@ -43,7 +43,6 @@ export default async function closeDrawing(this: DrawingStore, dispatcher: Actio await debounceSaveDrawing({ debounceTime: 0, retryOnError: false }) } - if (this.layer.config) { // flagging the layer as not edited anymore (not displayed on the map by the drawing module anymore) if (isOnlineMode(this.onlineMode)) { diff --git a/packages/viewer/src/utils/components/EmailInput.vue b/packages/viewer/src/utils/components/EmailInput.vue index 9857883d09..36d156983a 100644 --- a/packages/viewer/src/utils/components/EmailInput.vue +++ b/packages/viewer/src/utils/components/EmailInput.vue @@ -6,104 +6,100 @@ import { useComponentUniqueId } from '@/utils/composables/useComponentUniqueId' import { useFieldValidation } from '@/utils/composables/useFieldValidation' import { isValidEmail } from '@/utils/utils' -const props = withDefaults(defineProps<{ - /** Label to add above the field */ - label?: string - /** Description to add below the input */ - description?: string - /** Mark the field as disable */ - disabled?: boolean - /** - * Placeholder text - * - * NOTE: this should be a translation key - */ - placeholder?: string - /** Field is required and will be marked as invalid if empty */ - required?: boolean - /** - * Mark the field as valid - * - * This can be used if the field requires some external validation. When not set or set to - * undefined this props is ignored. - * - * NOTE: this props is ignored when activate-validation is false - */ - validMarker?: boolean | undefined - /** - * Valid message Message that will be added in green below the field once the validation has - * been done and the field is valid. - */ - validMessage?: string - /** - * Mark the field as invalid - * - * This can be used if the field requires some external validation. When not set or set to - * undefined this props is ignored. - * - * NOTE: this props is ignored when activate-validation is false - */ - invalidMarker?: boolean | undefined - /** - * Invalid message Message that will be added in red below the field once the validation has - * been done and the field is invalid. - * - * NOTE: this message is overwritten if the internal validation failed (not allow file type or - * file too big or required empty file) - */ - invalidMessage?: string - /** - * Mark the field has validated. - * - * As long as the flag is false, no validation is run and no validation marks are set. Also the - * props is-invalid and is-valid are ignored. - */ - activateValidation?: boolean - /** - * Validate function to run when the input changes The function should return an object of type - * `{valid: Boolean, invalidMessage: String}`. The `invalidMessage` string should be a - * translation key. - * - * NOTE: this function is called each time the field is modified - */ - validate?: ((_value?: string) => { valid: boolean; invalidMessage: string }) | undefined - dataCy?: string -}>(), { - validMarker: undefined, - invalidMarker: undefined, -}) +const props = withDefaults( + defineProps<{ + /** Label to add above the field */ + label?: string + /** Description to add below the input */ + description?: string + /** Mark the field as disable */ + disabled?: boolean + /** + * Placeholder text + * + * NOTE: this should be a translation key + */ + placeholder?: string + /** Field is required and will be marked as invalid if empty */ + required?: boolean + /** + * Mark the field as valid + * + * This can be used if the field requires some external validation. When not set or set to + * undefined this props is ignored. + * + * NOTE: this props is ignored when activate-validation is false + */ + validMarker?: boolean | undefined + /** + * Valid message Message that will be added in green below the field once the validation has + * been done and the field is valid. + */ + validMessage?: string + /** + * Mark the field as invalid + * + * This can be used if the field requires some external validation. When not set or set to + * undefined this props is ignored. + * + * NOTE: this props is ignored when activate-validation is false + */ + invalidMarker?: boolean | undefined + /** + * Invalid message Message that will be added in red below the field once the validation has + * been done and the field is invalid. + * + * NOTE: this message is overwritten if the internal validation failed (not allow file type + * or file too big or required empty file) + */ + invalidMessage?: string + /** + * Mark the field has validated. + * + * As long as the flag is false, no validation is run and no validation marks are set. Also + * the props is-invalid and is-valid are ignored. + */ + activateValidation?: boolean + /** + * Validate function to run when the input changes The function should return an object of + * type `{valid: Boolean, invalidMessage: String}`. The `invalidMessage` string should be a + * translation key. + * + * NOTE: this function is called each time the field is modified + */ + validate?: ((_value?: string) => { valid: boolean; invalidMessage: string }) | undefined + dataCy?: string + }>(), + { + validMarker: undefined, + invalidMarker: undefined, + } +) -const { - label, - description, - disabled, - placeholder, - dataCy, -} = toRefs(props) +const { label, description, disabled, placeholder, dataCy } = toRefs(props) const inputEmailId = useComponentUniqueId('email-input') const model = defineModel({ default: '' }) -const emits = defineEmits(['change', 'validate', 'focusin', 'focusout', 'keydown.enter']) +const emits = defineEmits<{ + change: [void] + validate: [isValid: boolean] + focusin: [void] + focusout: [void] + 'keydown.enter': [void] +}>() const { t } = useI18n() const { value, validMarker: computedValidMarker, invalidMarker: computedInvalidMarker, - validMessage: computedValidMessage, invalidMessage: computedInvalidMessage, - required: computedRequired, onFocus, -} = useFieldValidation( - props, - model, - emits as (_event: string, ..._args: unknown[]) => void, - { - customValidate: validateEmail, - requiredInvalidMessage: 'no_email', - } -) +} = useFieldValidation(props, model, emits, { + customValidate: validateEmail, + requiredInvalidMessage: 'no_email', +}) const emailInputElement = useTemplateRef('emailInputElement') @@ -129,7 +125,7 @@ defineExpose({ focus })