diff --git a/src/api/features/EditableFeature.class.js b/src/api/features/EditableFeature.class.js
index 989eac4c9..997642f38 100644
--- a/src/api/features/EditableFeature.class.js
+++ b/src/api/features/EditableFeature.class.js
@@ -4,7 +4,14 @@ import { extractOlFeatureGeodesicCoordinates } from '@/api/features/features.api
import SelectableFeature from '@/api/features/SelectableFeature.class'
import { DEFAULT_ICON_URL_PARAMS } from '@/api/icon.api'
import { DEFAULT_TITLE_OFFSET } from '@/api/icon.api'
-import { allStylingColors, allStylingSizes, MEDIUM, RED } from '@/utils/featureStyleUtils'
+import {
+ allStylingColors,
+ allStylingSizes,
+ allStylingTextPlacementsWithUnknown,
+ MEDIUM,
+ RED,
+ TOP,
+} from '@/utils/featureStyleUtils'
/** @enum */
export const EditableFeatureTypes = {
@@ -33,6 +40,19 @@ export default class EditableFeature extends SelectableFeature {
* @param {DrawingIcon} featureData.icon Icon that will be covering this feature, can be null
* @param {FeatureStyleSize} featureData.iconSize Size of the icon (if defined) that will be
* covering this feature
+ *
+ * @typedef {'top-left'
+ * | 'top'
+ * | 'top-right'
+ * | 'left'
+ * | 'center'
+ * | 'right'
+ * | 'bottom-left'
+ * | 'bottom'
+ * | 'bottom-right'
+ * | 'unknown'} TextPlacement
+ * @param {TextPlacement} featureData.textPlacement Size of the icon (if defined) that will be
+ * covering this feature
*/
constructor(featureData) {
const {
@@ -48,6 +68,7 @@ export default class EditableFeature extends SelectableFeature {
fillColor = RED,
icon = null,
iconSize = MEDIUM,
+ textPlacement = TOP,
} = featureData
super({ id, coordinates, title, description, geometry, isEditable: true })
this._featureType = featureType
@@ -59,6 +80,7 @@ export default class EditableFeature extends SelectableFeature {
this._iconSize = iconSize
this._geodesicCoordinates = null
this._isDragged = false
+ this._textPlacement = textPlacement
}
/**
@@ -108,6 +130,22 @@ export default class EditableFeature extends SelectableFeature {
}
}
+ /** @returns {TextPlacement} */
+ get textPlacement() {
+ return this._textPlacement
+ }
+
+ /** @param newPlacement {TextPlacement} */
+ set textPlacement(newPlacement) {
+ if (
+ newPlacement &&
+ allStylingTextPlacementsWithUnknown.find((placement) => placement === newPlacement)
+ ) {
+ this._textPlacement = newPlacement
+ this.emitStylingChangeEvent('textPlacement')
+ }
+ }
+
/** @returns {FeatureStyleSize} */
get textSize() {
return this._textSize
diff --git a/src/modules/i18n/locales/de.json b/src/modules/i18n/locales/de.json
index 01b168297..5f085d0a6 100644
--- a/src/modules/i18n/locales/de.json
+++ b/src/modules/i18n/locales/de.json
@@ -419,6 +419,7 @@
"modify_new_vertex_measure": "Klicke, um einen Punkt hinzuzufügen.
Punkt verschieben: klicken und ziehen ",
"modify_text_color_label": "Textfarbe",
"modify_text_label": "Text",
+ "modify_text_placement_label": "Platzierung",
"modify_text_size_label": "Grösse",
"more_info": "Mehr dazu ...",
"movie": "Zeitreihen (Multi-PDF)",
diff --git a/src/modules/i18n/locales/en.json b/src/modules/i18n/locales/en.json
index 02a39eba1..53ec5491c 100644
--- a/src/modules/i18n/locales/en.json
+++ b/src/modules/i18n/locales/en.json
@@ -419,6 +419,7 @@
"modify_new_vertex_measure": "Click to add a point
Click then drag to move the point",
"modify_text_color_label": "Text color",
"modify_text_label": "Text",
+ "modify_text_placement_label": "Platzierung",
"modify_text_size_label": "Size",
"more_info": "More info ...",
"movie": "Time serie (Mutliple PDF)",
diff --git a/src/modules/i18n/locales/fr.json b/src/modules/i18n/locales/fr.json
index 322fb848c..e655ce297 100644
--- a/src/modules/i18n/locales/fr.json
+++ b/src/modules/i18n/locales/fr.json
@@ -419,6 +419,7 @@
"modify_new_vertex_measure": " Cliquer pour ajouter un point
Cliquer puis bouger le curseur pour déplacer le point",
"modify_text_color_label": "Couleur du texte",
"modify_text_label": "Texte",
+ "modify_text_placement_label": "Emplacement",
"modify_text_size_label": "Taille",
"more_info": "Plus d'informations ...",
"movie": "Série temporelle (Multi-PDF)",
diff --git a/src/modules/i18n/locales/it.json b/src/modules/i18n/locales/it.json
index aec935493..634417949 100644
--- a/src/modules/i18n/locales/it.json
+++ b/src/modules/i18n/locales/it.json
@@ -419,6 +419,7 @@
"modify_new_vertex_measure": "Cliccare per aggiungere un punto.
Cliccare quindi trascinare per spostare il punto",
"modify_text_color_label": "Colore del testo",
"modify_text_label": "Nota",
+ "modify_text_placement_label": "Posizione",
"modify_text_size_label": "Grandezza",
"more_info": "Più informazioni ...",
"movie": "Serie storica (Multi-PDF)",
diff --git a/src/modules/i18n/locales/rm.json b/src/modules/i18n/locales/rm.json
index a16f13d11..a519fe041 100644
--- a/src/modules/i18n/locales/rm.json
+++ b/src/modules/i18n/locales/rm.json
@@ -417,6 +417,7 @@
"modify_new_vertex_measure": "Cliccar per agiuntar punct. Spustar punct :cliccar e trair",
"modify_text_color_label": "Colur dal text",
"modify_text_label": "Etichetta",
+ "modify_text_placement_label": "Plazzament",
"modify_text_size_label": "Grondezza",
"more_info": "Ulteriuras infurmaziuns ...",
"movie": "Seria temporala (Multi-PDF)",
diff --git a/src/modules/infobox/components/styling/DrawingStyleIconSelector.vue b/src/modules/infobox/components/styling/DrawingStyleIconSelector.vue
index 1518d3f86..75d44dc09 100644
--- a/src/modules/infobox/components/styling/DrawingStyleIconSelector.vue
+++ b/src/modules/infobox/components/styling/DrawingStyleIconSelector.vue
@@ -18,7 +18,6 @@
/>
-
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/infobox/components/styling/FeatureStyleEdit.vue b/src/modules/infobox/components/styling/FeatureStyleEdit.vue
index c3dfd0855..a7046e073 100644
--- a/src/modules/infobox/components/styling/FeatureStyleEdit.vue
+++ b/src/modules/infobox/components/styling/FeatureStyleEdit.vue
@@ -10,6 +10,7 @@ import FeatureAreaInfo from '@/modules/infobox/components/FeatureAreaInfo.vue'
import DrawingStyleColorSelector from '@/modules/infobox/components/styling/DrawingStyleColorSelector.vue'
import DrawingStyleIconSelector from '@/modules/infobox/components/styling/DrawingStyleIconSelector.vue'
import DrawingStyleMediaLink from '@/modules/infobox/components/styling/DrawingStyleMediaLink.vue'
+import DrawingStylePositionSelector from '@/modules/infobox/components/styling/DrawingStylePlacementSelector.vue'
import DrawingStylePopoverButton from '@/modules/infobox/components/styling/DrawingStylePopoverButton.vue'
import DrawingStyleSizeSelector from '@/modules/infobox/components/styling/DrawingStyleSizeSelector.vue'
import DrawingStyleTextColorSelector from '@/modules/infobox/components/styling/DrawingStyleTextColorSelector.vue'
@@ -36,7 +37,6 @@ const { feature, readOnly } = toRefs(props)
const title = ref(feature.value.title)
const description = ref(feature.value.description)
const mediaPopovers = ref(null)
-
const isEditingText = computed(() => {
const titleElement = document.getElementById('drawing-style-feature-title')
const descriptionElement = document.getElementById('drawing-style-feature-description')
@@ -95,6 +95,10 @@ function updateFeatureTitle() {
title: title.value.trim(),
...dispatcher,
})
+ // Update the text offset if the feature is a marker
+ if (feature.value.featureType === EditableFeatureTypes.MARKER) {
+ updateTextOffset()
+ }
}
function updateFeatureDescription() {
@@ -129,6 +133,14 @@ function onTextSizeChange(textSize) {
store.dispatch('changeFeatureTextSize', { feature: feature.value, textSize, ...dispatcher })
updateTextOffset()
}
+function onPlacementChange(textPlacement) {
+ store.dispatch('changeFeatureTextPlacement', {
+ feature: feature.value,
+ textPlacement,
+ ...dispatcher,
+ })
+ updateTextOffset()
+}
function onTextColorChange(textColor) {
store.dispatch('changeFeatureTextColor', { feature: feature.value, textColor, ...dispatcher })
}
@@ -157,7 +169,9 @@ function updateTextOffset() {
feature.value.textSize.textScale,
feature.value.iconSize.iconScale,
feature.value.icon.anchor,
- feature.value.icon.size
+ feature.value.icon.size,
+ feature.value.textPlacement,
+ title.value
)
store.dispatch('changeFeatureTextOffset', {
@@ -275,6 +289,12 @@ function mediaTypes() {
:current-size="feature.textSize"
@change="onTextSizeChange"
/>
+
position === textPlacement
+ )
+ if (wantedPlacement && selectedFeature && selectedFeature.isEditable) {
+ commit('changeFeatureTextPlacement', {
+ feature: selectedFeature,
+ textPlacement: wantedPlacement,
+ dispatcher,
+ })
+ }
+ },
/**
* Changes the text offset of the feature. Only change the text offset if the feature is
* editable and part of the currently selected features
@@ -735,6 +763,9 @@ export default {
changeFeatureTextSize(state, { feature, textSize }) {
feature.textSize = textSize
},
+ changeFeatureTextPlacement(state, { feature, textPlacement }) {
+ feature.textPlacement = textPlacement
+ },
changeFeatureTextOffset(state, { feature, textOffset }) {
feature.textOffset = textOffset
},
diff --git a/src/utils/featureStyleUtils.js b/src/utils/featureStyleUtils.js
index 4722dacec..f47d8b8f5 100644
--- a/src/utils/featureStyleUtils.js
+++ b/src/utils/featureStyleUtils.js
@@ -4,7 +4,6 @@ import Style from 'ol/style/Style'
import { EditableFeatureTypes } from '@/api/features/EditableFeature.class'
import { DEFAULT_TITLE_OFFSET } from '@/api/icon.api'
-import log from '@/utils/logging'
import { dashedRedStroke, whiteSketchFill } from '@/utils/styleUtils.js'
/** A color that can be used to style a feature (comprised of a fill and a border color) */
@@ -70,7 +69,8 @@ export const WHITE = new FeatureStyleColor('white', '#ffffff', '#000000')
export const YELLOW = new FeatureStyleColor('yellow', '#ffff00', '#000000')
export const allStylingColors = [BLACK, BLUE, GRAY, GREEN, ORANGE, RED, WHITE, YELLOW]
-
+export const FEATURE_FONT_SIZE = 16
+export const FEATURE_FONT = 'Helvetica'
/**
* Representation of a size for feature style
*
@@ -118,7 +118,7 @@ export class FeatureStyleSize {
}
get font() {
- return `normal ${16 * this.textScale}px Helvetica`
+ return `normal ${FEATURE_FONT_SIZE * this.textScale}px ${FEATURE_FONT}`
}
}
@@ -132,12 +132,35 @@ export const MEDIUM = new FeatureStyleSize('medium_size', 1.5, 0.75)
export const LARGE = new FeatureStyleSize('large_size', 2.0, 1)
export const EXTRA_LARGE = new FeatureStyleSize('extra_large_size', 2.5, 1.25)
+export const LEFT = 'left'
+export const RIGHT = 'right'
+export const TOP = 'top'
+export const BOTTOM = 'bottom'
+export const CENTER = 'center'
+export const UNKNOWN = 'unknown'
+export const TOP_LEFT = 'top-left'
+export const TOP_RIGHT = 'top-right'
+export const BOTTOM_LEFT = 'bottom-left'
+export const BOTTOM_RIGHT = 'bottom-right'
+
/**
* List of all available sizes for drawing style
*
* @type {FeatureStyleSize[]}
*/
export const allStylingSizes = [SMALL, MEDIUM, LARGE, EXTRA_LARGE]
+export const allStylingTextPlacements = [
+ TOP_LEFT,
+ TOP,
+ TOP_RIGHT,
+ LEFT,
+ CENTER,
+ RIGHT,
+ BOTTOM_LEFT,
+ BOTTOM,
+ BOTTOM_RIGHT,
+]
+export const allStylingTextPlacementsWithUnknown = [...allStylingTextPlacements, UNKNOWN]
/**
* Get Feature style from feature
@@ -212,23 +235,91 @@ export function getTextColor(style) {
* @param {Number} iconScale Icon scaling
* @param {Array} anchor Relative position of Anchor
* @param {Array} iconSize Absolute size of icon in pixel
- * @returns {Array | null} Returns the feature label offset
+ *
+ * @typedef {'top-left' | 'top' | 'top-right', | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right' | 'unknown'} TextPlacement
+ * @param {TextPlacement} textPlacement Absolute position of text in pixel
+ * @returns {Array} Returns the feature label offset
*/
-export function calculateTextOffset(textScale, iconScale, anchor, iconSize) {
+export function calculateTextOffset(textScale, iconScale, anchor, iconSize, textPlacement, text) {
if (!iconScale) {
return DEFAULT_TITLE_OFFSET
}
+ const [textPlacementX, textPlacementY] = calculateTextXYOffset(
+ textScale,
+ iconScale,
+ anchor,
+ iconSize,
+ text
+ )
+ return calculateTextOffsetFromPlacement(textPlacementX, textPlacementY, textPlacement)
+}
+
+/**
+ * Calculate the text X and Y offset that can be applied to the text depending on the text position
+ *
+ * @param {Number} textScale Text scaling
+ * @param {Number} iconScale Icon scaling
+ * @param {Array} anchor Relative position of Anchor
+ * @param {Array} iconSize Absolute size of icon in pixel
+ * @param {String} text Text to display
+ * @returns {Array} Returns the default X and Y label offset in pixel
+ */
+export function calculateTextXYOffset(textScale, iconScale, anchor, iconSize, text) {
const fontSize = 11
- let anchorScale = anchor ? anchor[1] * 2 : 1
+ const anchorScale = anchor ? anchor[1] * 2 : 1
const iconOffset = 0.5 * iconScale * anchorScale * iconSize[1]
const textOffset = 0.5 * fontSize * textScale
+ const textWidth = calculateFeatureTextWidth(text, textScale)
const defaultOffset = 5
- const offset = [0, -(defaultOffset + iconOffset + textOffset)]
- log.debug('title offset of feature is calculated to be : ', offset)
- return offset
+ return [
+ defaultOffset + iconOffset + textOffset + textWidth / 2, // / 2 because the text is centered so the textWidth has to be halved
+ defaultOffset + iconOffset + textOffset,
+ ]
+}
+
+/**
+ * Calculate the text offset from the text placement and the default offset
+ *
+ * @param {Number} defaultXOffset Default X offset
+ * @param {Number} defaultYOffset Default Y offset
+ * @param {String} placement Text placement
+ * @returns {Array} Returns the default X and Y label offset in pixel
+ */
+export function calculateTextOffsetFromPlacement(defaultXOffset, defaultYOffset, placement) {
+ const offsets = {
+ [TOP_LEFT]: [-defaultXOffset, -defaultYOffset],
+ [TOP]: [0, -defaultYOffset],
+ [TOP_RIGHT]: [defaultXOffset, -defaultYOffset],
+ [LEFT]: [-defaultXOffset, 0],
+ [CENTER]: [0, 0],
+ [RIGHT]: [defaultXOffset, 0],
+ [BOTTOM_LEFT]: [-defaultXOffset, defaultYOffset],
+ [BOTTOM]: [0, defaultYOffset],
+ [BOTTOM_RIGHT]: [defaultXOffset, defaultYOffset],
+ }
+
+ return offsets[placement] || [0, 0]
+}
+
+/**
+ * Calculates the width of a feature text given a text and a text scale
+ *
+ * @param {String} text
+ * @param {Number} textScale
+ * @returns
+ */
+export function calculateFeatureTextWidth(text, textScale) {
+ const canvas = document.createElement('canvas')
+ const context = canvas.getContext('2d')
+ // In unit tests the context is not available
+ if (!context) {
+ return 0
+ }
+ context.font = `normal ${FEATURE_FONT_SIZE * textScale}px ${FEATURE_FONT}`
+ return context.measureText(text).width
}
/**
diff --git a/src/utils/kmlUtils.js b/src/utils/kmlUtils.js
index 505c850df..425e1a7ed 100644
--- a/src/utils/kmlUtils.js
+++ b/src/utils/kmlUtils.js
@@ -18,6 +18,9 @@ import KmlStyles from '@/api/layers/KmlStyles.enum'
import { WGS84 } from '@/utils/coordinates/coordinateSystems'
import {
allStylingSizes,
+ allStylingTextPlacements,
+ calculateTextOffsetFromPlacement,
+ calculateTextXYOffset,
geoadminStyleFunction,
getFeatureStyleColor,
getStyle,
@@ -25,6 +28,7 @@ import {
getTextSize,
RED,
SMALL,
+ UNKNOWN,
} from '@/utils/featureStyleUtils'
import { GeodesicGeometries } from '@/utils/geodesicManager'
import log from '@/utils/logging'
@@ -413,7 +417,14 @@ export function getEditableFeatureFromKmlFeature(kmlFeature, availableIconSets)
const geometry = new GeoJSON().writeGeometryObject(kmlFeature.getGeometry())
const coordinates = extractOlFeatureCoordinates(kmlFeature)
-
+ const textPlacement = detectTextPlacement(
+ textScale,
+ iconStyle?.getScale(),
+ icon?.anchor,
+ icon?.size,
+ title,
+ textOffset
+ )
if (iconArgs?.isLegacy && iconStyle && icon) {
// The legacy drawing uses icons from old URLs, some of them have already been removed
// like the versioned URLs (/{version}/img/maki/{image}-{size}@{scale}x.png) while others
@@ -452,9 +463,46 @@ export function getEditableFeatureFromKmlFeature(kmlFeature, availableIconSets)
fillColor,
icon,
iconSize,
+ textPlacement,
})
}
+/**
+ * Detect the feature text placement based on the icon and text size
+ *
+ * @param {Number} textScale Text scaling
+ * @param {Number} iconScale Icon scaling
+ * @param {Array} anchor Relative position of Anchor
+ * @param {Array} iconSize Absolute size of icon in pixel
+ * @param {String} text Text to display
+ * @param {Array} currentTextOffset Current text offset of the kml feature
+ * @returns {TextPlacement} Returns the text placement or undefined if the icon is not a marker
+ */
+function detectTextPlacement(textScale, iconScale, anchor, iconSize, text, currentTextOffset) {
+ if (!text || !textScale || !iconScale || !anchor || !iconSize) {
+ return UNKNOWN
+ }
+ const [textPlacementX, textPlacementY] = calculateTextXYOffset(
+ textScale,
+ iconScale,
+ anchor,
+ iconSize,
+ text
+ )
+
+ for (let placementOption of allStylingTextPlacements) {
+ const [xOffset, yOffset] = calculateTextOffsetFromPlacement(
+ textPlacementX,
+ textPlacementY,
+ placementOption
+ )
+ if (xOffset === currentTextOffset[0] && yOffset === currentTextOffset[1]) {
+ return placementOption
+ }
+ }
+ return UNKNOWN
+}
+
const nonGeoadminIconUrls = new Set()
export function iconUrlProxyFy(url, corsIssueCallback = null, httpIssueCallBack = null) {
// We only proxyfy URL that are not from our backend.
diff --git a/tests/cypress/tests-e2e/drawing.cy.js b/tests/cypress/tests-e2e/drawing.cy.js
index c7765b9cb..2a9edc39f 100644
--- a/tests/cypress/tests-e2e/drawing.cy.js
+++ b/tests/cypress/tests-e2e/drawing.cy.js
@@ -238,6 +238,32 @@ describe('Drawing module tests', () => {
// changing/editing the title of this marker
testTitleEdit()
+ // changing text placement
+ cy.log('Test text placement and offset')
+ cy.get('[data-cy="drawing-style-text-button"]').click()
+ cy.get('[data-cy="drawing-style-placement-selector-top-left"]').click()
+ cy.readStoreValue('getters.selectedFeatures[0].textPlacement').should(
+ 'eq',
+ 'top-left'
+ )
+ cy.readStoreValue('getters.selectedFeatures[0].textOffset').then((offset) => {
+ cy.wrap(offset[0]).should('be.lessThan', 0)
+ cy.wrap(offset[1]).should('be.lessThan', 0)
+ })
+
+ cy.wait('@update-kml')
+ .its('request')
+ .should((request) =>
+ checkKMLRequest(request, [
+ new RegExp(
+ `` +
+ `(-?\\d+\\.\\d+),(-?\\d+\\.\\d+)` + // Test if both values are floats
+ ``
+ ),
+ ])
+ )
+ cy.get('[data-cy="drawing-style-text-button"]').click()
+
// changing/editing the description of this marker
const description = 'A description for this marker'
cy.get('[data-cy="drawing-style-feature-description"]').type(description)
@@ -520,6 +546,7 @@ describe('Drawing module tests', () => {
// Opening text style edit popup
cy.get('[data-cy="drawing-style-text-button"]').click()
+ cy.get('[data-cy="drawing-style-placement-selector-top-left"]').should('not.exist')
cy.get('[data-cy="drawing-style-text-popup"]').should('be.visible')
// all available colors must have a dedicated element/button